/* Copyright (c) Mark Harmstone 2016-17
 *
 * This file is part of WinBtrfs.
 *
 * WinBtrfs is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public Licence as published by
 * the Free Software Foundation, either version 3 of the Licence, or
 * (at your option) any later version.
 *
 * WinBtrfs is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public Licence for more details.
 *
 * You should have received a copy of the GNU Lesser General Public Licence
 * along with WinBtrfs.  If not, see <http://www.gnu.org/licenses/>. */

#include "btrfs_drv.h"
#include "crc32c.h"

// not currently in mingw
#ifndef _MSC_VER
#define FileIdExtdDirectoryInformation (enum _FILE_INFORMATION_CLASS)60
#define FileIdExtdBothDirectoryInformation (enum _FILE_INFORMATION_CLASS)63

typedef struct _FILE_ID_EXTD_DIR_INFORMATION {
    ULONG NextEntryOffset;
    ULONG FileIndex;
    LARGE_INTEGER CreationTime;
    LARGE_INTEGER LastAccessTime;
    LARGE_INTEGER LastWriteTime;
    LARGE_INTEGER ChangeTime;
    LARGE_INTEGER EndOfFile;
    LARGE_INTEGER AllocationSize;
    ULONG FileAttributes;
    ULONG FileNameLength;
    ULONG EaSize;
    ULONG ReparsePointTag;
    FILE_ID_128 FileId;
    WCHAR FileName[1];
} FILE_ID_EXTD_DIR_INFORMATION, *PFILE_ID_EXTD_DIR_INFORMATION;

typedef struct _FILE_ID_EXTD_BOTH_DIR_INFORMATION {
    ULONG NextEntryOffset;
    ULONG FileIndex;
    LARGE_INTEGER CreationTime;
    LARGE_INTEGER LastAccessTime;
    LARGE_INTEGER LastWriteTime;
    LARGE_INTEGER ChangeTime;
    LARGE_INTEGER EndOfFile;
    LARGE_INTEGER AllocationSize;
    ULONG FileAttributes;
    ULONG FileNameLength;
    ULONG EaSize;
    ULONG ReparsePointTag;
    FILE_ID_128 FileId;
    CCHAR ShortNameLength;
    WCHAR ShortName[12];
    WCHAR FileName[1];
} FILE_ID_EXTD_BOTH_DIR_INFORMATION, *PFILE_ID_EXTD_BOTH_DIR_INFORMATION;

#endif

enum DirEntryType {
    DirEntryType_File,
    DirEntryType_Self,
    DirEntryType_Parent
};

typedef struct {
    KEY key;
    UNICODE_STRING name;
    uint8_t type;
    enum DirEntryType dir_entry_type;
    dir_child* dc;
} dir_entry;

ULONG get_reparse_tag_fcb(fcb* fcb) {
    ULONG tag;

    if (fcb->type == BTRFS_TYPE_SYMLINK)
        return IO_REPARSE_TAG_SYMLINK;
    else if (fcb->type == BTRFS_TYPE_DIRECTORY) {
        if (!fcb->reparse_xattr.Buffer || fcb->reparse_xattr.Length < sizeof(ULONG))
            return 0;

        RtlCopyMemory(&tag, fcb->reparse_xattr.Buffer, sizeof(ULONG));
    } else {
        NTSTATUS Status;
        ULONG br;

        Status = read_file(fcb, (uint8_t*)&tag, 0, sizeof(ULONG), &br, NULL);
        if (!NT_SUCCESS(Status)) {
            ERR("read_file returned %08lx\n", Status);
            return 0;
        }
    }

    return tag;
}

ULONG get_reparse_tag(device_extension* Vcb, root* subvol, uint64_t inode, uint8_t type, ULONG atts, bool lxss, PIRP Irp) {
    fcb* fcb;
    ULONG tag = 0;
    NTSTATUS Status;

    if (type == BTRFS_TYPE_SYMLINK)
        return IO_REPARSE_TAG_SYMLINK;
    else if (lxss) {
        if (type == BTRFS_TYPE_SOCKET)
            return IO_REPARSE_TAG_AF_UNIX;
        else if (type == BTRFS_TYPE_FIFO)
            return IO_REPARSE_TAG_LX_FIFO;
        else if (type == BTRFS_TYPE_CHARDEV)
            return IO_REPARSE_TAG_LX_CHR;
        else if (type == BTRFS_TYPE_BLOCKDEV)
            return IO_REPARSE_TAG_LX_BLK;
    }

    if (type != BTRFS_TYPE_FILE && type != BTRFS_TYPE_DIRECTORY)
        return 0;

    if (!(atts & FILE_ATTRIBUTE_REPARSE_POINT))
        return 0;

    Status = open_fcb(Vcb, subvol, inode, type, NULL, false, NULL, &fcb, PagedPool, Irp);
    if (!NT_SUCCESS(Status)) {
        ERR("open_fcb returned %08lx\n", Status);
        return 0;
    }

    ExAcquireResourceSharedLite(fcb->Header.Resource, true);

    tag = get_reparse_tag_fcb(fcb);

    ExReleaseResourceLite(fcb->Header.Resource);

    free_fcb(fcb);

    return tag;
}

static ULONG get_ea_len(device_extension* Vcb, root* subvol, uint64_t inode, PIRP Irp) {
    uint8_t* eadata;
    uint16_t len;

    if (get_xattr(Vcb, subvol, inode, EA_EA, EA_EA_HASH, &eadata, &len, Irp)) {
        ULONG offset;
        NTSTATUS Status;

        Status = IoCheckEaBufferValidity((FILE_FULL_EA_INFORMATION*)eadata, len, &offset);

        if (!NT_SUCCESS(Status)) {
            WARN("IoCheckEaBufferValidity returned %08lx (error at offset %lu)\n", Status, offset);
            ExFreePool(eadata);
            return 0;
        } else {
            FILE_FULL_EA_INFORMATION* eainfo;
            ULONG ealen;

            ealen = 4;
            eainfo = (FILE_FULL_EA_INFORMATION*)eadata;
            do {
                ealen += 5 + eainfo->EaNameLength + eainfo->EaValueLength;

                if (eainfo->NextEntryOffset == 0)
                    break;

                eainfo = (FILE_FULL_EA_INFORMATION*)(((uint8_t*)eainfo) + eainfo->NextEntryOffset);
            } while (true);

            ExFreePool(eadata);

            return ealen;
        }
    } else
        return 0;
}

static NTSTATUS query_dir_item(fcb* fcb, ccb* ccb, void* buf, LONG* len, PIRP Irp, dir_entry* de, root* r) {
    PIO_STACK_LOCATION IrpSp;
    LONG needed;
    uint64_t inode;
    INODE_ITEM ii;
    NTSTATUS Status;
    ULONG atts = 0, ealen = 0;
    file_ref* fileref = ccb->fileref;

    IrpSp = IoGetCurrentIrpStackLocation(Irp);

    if (de->key.obj_type == TYPE_ROOT_ITEM) { // subvol
        LIST_ENTRY* le;

        r = NULL;

        le = fcb->Vcb->roots.Flink;
        while (le != &fcb->Vcb->roots) {
            root* subvol = CONTAINING_RECORD(le, root, list_entry);

            if (subvol->id == de->key.obj_id) {
                r = subvol;
                break;
            }

            le = le->Flink;
        }

        if (r && r->parent != fcb->subvol->id && (!de->dc || !de->dc->root_dir))
            r = NULL;

        inode = SUBVOL_ROOT_INODE;
    } else {
        inode = de->key.obj_id;
    }

    if (IrpSp->Parameters.QueryDirectory.FileInformationClass != FileNamesInformation) { // FIXME - object ID and reparse point classes too?
        switch (de->dir_entry_type) {
            case DirEntryType_File:
            {
                if (!r) {
                    LARGE_INTEGER time;

                    ii = fcb->Vcb->dummy_fcb->inode_item;
                    atts = fcb->Vcb->dummy_fcb->atts;
                    ealen = fcb->Vcb->dummy_fcb->ealen;

                    KeQuerySystemTime(&time);
                    win_time_to_unix(time, &ii.otime);
                    ii.st_atime = ii.st_mtime = ii.st_ctime = ii.otime;
                } else {
                    bool found = false;

                    if (de->dc && de->dc->fileref && de->dc->fileref->fcb) {
                        ii = de->dc->fileref->fcb->inode_item;
                        atts = de->dc->fileref->fcb->atts;
                        ealen = de->dc->fileref->fcb->ealen;
                        found = true;
                    }

                    if (!found) {
                        KEY searchkey;
                        traverse_ptr tp;

                        searchkey.obj_id = inode;
                        searchkey.obj_type = TYPE_INODE_ITEM;
                        searchkey.offset = 0xffffffffffffffff;

                        Status = find_item(fcb->Vcb, r, &tp, &searchkey, false, Irp);
                        if (!NT_SUCCESS(Status)) {
                            ERR("error - find_item returned %08lx\n", Status);
                            return Status;
                        }

                        if (tp.item->key.obj_id != searchkey.obj_id || tp.item->key.obj_type != searchkey.obj_type) {
                            ERR("could not find inode item for inode %I64x in root %I64x\n", inode, r->id);
                            return STATUS_INTERNAL_ERROR;
                        }

                        RtlZeroMemory(&ii, sizeof(INODE_ITEM));

                        if (tp.item->size > 0)
                            RtlCopyMemory(&ii, tp.item->data, min(sizeof(INODE_ITEM), tp.item->size));

                        if (IrpSp->Parameters.QueryDirectory.FileInformationClass == FileBothDirectoryInformation ||
                            IrpSp->Parameters.QueryDirectory.FileInformationClass == FileDirectoryInformation ||
                            IrpSp->Parameters.QueryDirectory.FileInformationClass == FileFullDirectoryInformation ||
                            IrpSp->Parameters.QueryDirectory.FileInformationClass == FileIdBothDirectoryInformation ||
                            IrpSp->Parameters.QueryDirectory.FileInformationClass == FileIdFullDirectoryInformation ||
                            IrpSp->Parameters.QueryDirectory.FileInformationClass == FileIdExtdDirectoryInformation ||
                            IrpSp->Parameters.QueryDirectory.FileInformationClass == FileIdExtdBothDirectoryInformation) {

                            bool dotfile = de->name.Length > sizeof(WCHAR) && de->name.Buffer[0] == '.';

                            atts = get_file_attributes(fcb->Vcb, r, inode, de->type, dotfile, false, Irp);
                        }

                        if (IrpSp->Parameters.QueryDirectory.FileInformationClass == FileBothDirectoryInformation ||
                            IrpSp->Parameters.QueryDirectory.FileInformationClass == FileFullDirectoryInformation ||
                            IrpSp->Parameters.QueryDirectory.FileInformationClass == FileIdBothDirectoryInformation ||
                            IrpSp->Parameters.QueryDirectory.FileInformationClass == FileIdFullDirectoryInformation ||
                            IrpSp->Parameters.QueryDirectory.FileInformationClass == FileIdExtdDirectoryInformation ||
                            IrpSp->Parameters.QueryDirectory.FileInformationClass == FileIdExtdBothDirectoryInformation) {
                            ealen = get_ea_len(fcb->Vcb, r, inode, Irp);
                        }
                    }
                }

                break;
            }

            case DirEntryType_Self:
                ii = fcb->inode_item;
                r = fcb->subvol;
                inode = fcb->inode;
                atts = fcb->atts;
                ealen = fcb->ealen;
                break;

            case DirEntryType_Parent:
                if (fileref && fileref->parent) {
                    ii = fileref->parent->fcb->inode_item;
                    r = fileref->parent->fcb->subvol;
                    inode = fileref->parent->fcb->inode;
                    atts = fileref->parent->fcb->atts;
                    ealen = fileref->parent->fcb->ealen;
                } else {
                    ERR("no fileref\n");
                    return STATUS_INTERNAL_ERROR;
                }
                break;
        }

        if (atts == 0)
            atts = FILE_ATTRIBUTE_NORMAL;
    }

    switch (IrpSp->Parameters.QueryDirectory.FileInformationClass) {
        case FileBothDirectoryInformation:
        {
            FILE_BOTH_DIR_INFORMATION* fbdi = buf;

            TRACE("FileBothDirectoryInformation\n");

            needed = sizeof(FILE_BOTH_DIR_INFORMATION) - sizeof(WCHAR) + de->name.Length;

            if (needed > *len) {
                TRACE("buffer overflow - %li > %lu\n", needed, *len);
                return STATUS_BUFFER_OVERFLOW;
            }

            fbdi->NextEntryOffset = 0;
            fbdi->FileIndex = 0;
            fbdi->CreationTime.QuadPart = unix_time_to_win(&ii.otime);
            fbdi->LastAccessTime.QuadPart = unix_time_to_win(&ii.st_atime);
            fbdi->LastWriteTime.QuadPart = unix_time_to_win(&ii.st_mtime);
            fbdi->ChangeTime.QuadPart = unix_time_to_win(&ii.st_ctime);
            fbdi->EndOfFile.QuadPart = de->type == BTRFS_TYPE_SYMLINK ? 0 : ii.st_size;

            if (de->type == BTRFS_TYPE_SYMLINK)
                fbdi->AllocationSize.QuadPart = 0;
            else if (atts & FILE_ATTRIBUTE_SPARSE_FILE)
                fbdi->AllocationSize.QuadPart = ii.st_blocks;
            else
                fbdi->AllocationSize.QuadPart = sector_align(ii.st_size, fcb->Vcb->superblock.sector_size);

            fbdi->FileAttributes = atts;
            fbdi->FileNameLength = de->name.Length;
            fbdi->EaSize = (r && atts & FILE_ATTRIBUTE_REPARSE_POINT) ? get_reparse_tag(fcb->Vcb, r, inode, de->type, atts, ccb->lxss, Irp) : ealen;
            fbdi->ShortNameLength = 0;

            RtlCopyMemory(fbdi->FileName, de->name.Buffer, de->name.Length);

            *len -= needed;

            return STATUS_SUCCESS;
        }

        case FileDirectoryInformation:
        {
            FILE_DIRECTORY_INFORMATION* fdi = buf;

            TRACE("FileDirectoryInformation\n");

            needed = sizeof(FILE_DIRECTORY_INFORMATION) - sizeof(WCHAR) + de->name.Length;

            if (needed > *len) {
                TRACE("buffer overflow - %li > %lu\n", needed, *len);
                return STATUS_BUFFER_OVERFLOW;
            }

            fdi->NextEntryOffset = 0;
            fdi->FileIndex = 0;
            fdi->CreationTime.QuadPart = unix_time_to_win(&ii.otime);
            fdi->LastAccessTime.QuadPart = unix_time_to_win(&ii.st_atime);
            fdi->LastWriteTime.QuadPart = unix_time_to_win(&ii.st_mtime);
            fdi->ChangeTime.QuadPart = unix_time_to_win(&ii.st_ctime);
            fdi->EndOfFile.QuadPart = de->type == BTRFS_TYPE_SYMLINK ? 0 : ii.st_size;

            if (de->type == BTRFS_TYPE_SYMLINK)
                fdi->AllocationSize.QuadPart = 0;
            else if (atts & FILE_ATTRIBUTE_SPARSE_FILE)
                fdi->AllocationSize.QuadPart = ii.st_blocks;
            else
                fdi->AllocationSize.QuadPart = sector_align(ii.st_size, fcb->Vcb->superblock.sector_size);

            fdi->FileAttributes = atts;
            fdi->FileNameLength = de->name.Length;

            RtlCopyMemory(fdi->FileName, de->name.Buffer, de->name.Length);

            *len -= needed;

            return STATUS_SUCCESS;
        }

        case FileFullDirectoryInformation:
        {
            FILE_FULL_DIR_INFORMATION* ffdi = buf;

            TRACE("FileFullDirectoryInformation\n");

            needed = sizeof(FILE_FULL_DIR_INFORMATION) - sizeof(WCHAR) + de->name.Length;

            if (needed > *len) {
                TRACE("buffer overflow - %li > %lu\n", needed, *len);
                return STATUS_BUFFER_OVERFLOW;
            }

            ffdi->NextEntryOffset = 0;
            ffdi->FileIndex = 0;
            ffdi->CreationTime.QuadPart = unix_time_to_win(&ii.otime);
            ffdi->LastAccessTime.QuadPart = unix_time_to_win(&ii.st_atime);
            ffdi->LastWriteTime.QuadPart = unix_time_to_win(&ii.st_mtime);
            ffdi->ChangeTime.QuadPart = unix_time_to_win(&ii.st_ctime);
            ffdi->EndOfFile.QuadPart = de->type == BTRFS_TYPE_SYMLINK ? 0 : ii.st_size;

            if (de->type == BTRFS_TYPE_SYMLINK)
                ffdi->AllocationSize.QuadPart = 0;
            else if (atts & FILE_ATTRIBUTE_SPARSE_FILE)
                ffdi->AllocationSize.QuadPart = ii.st_blocks;
            else
                ffdi->AllocationSize.QuadPart = sector_align(ii.st_size, fcb->Vcb->superblock.sector_size);

            ffdi->FileAttributes = atts;
            ffdi->FileNameLength = de->name.Length;
            ffdi->EaSize = (r && atts & FILE_ATTRIBUTE_REPARSE_POINT) ? get_reparse_tag(fcb->Vcb, r, inode, de->type, atts, ccb->lxss, Irp) : ealen;

            RtlCopyMemory(ffdi->FileName, de->name.Buffer, de->name.Length);

            *len -= needed;

            return STATUS_SUCCESS;
        }

        case FileIdBothDirectoryInformation:
        {
            FILE_ID_BOTH_DIR_INFORMATION* fibdi = buf;

            TRACE("FileIdBothDirectoryInformation\n");

            needed = sizeof(FILE_ID_BOTH_DIR_INFORMATION) - sizeof(WCHAR) + de->name.Length;

            if (needed > *len) {
                TRACE("buffer overflow - %li > %lu\n", needed, *len);
                return STATUS_BUFFER_OVERFLOW;
            }

            fibdi->NextEntryOffset = 0;
            fibdi->FileIndex = 0;
            fibdi->CreationTime.QuadPart = unix_time_to_win(&ii.otime);
            fibdi->LastAccessTime.QuadPart = unix_time_to_win(&ii.st_atime);
            fibdi->LastWriteTime.QuadPart = unix_time_to_win(&ii.st_mtime);
            fibdi->ChangeTime.QuadPart = unix_time_to_win(&ii.st_ctime);
            fibdi->EndOfFile.QuadPart = de->type == BTRFS_TYPE_SYMLINK ? 0 : ii.st_size;

            if (de->type == BTRFS_TYPE_SYMLINK)
                fibdi->AllocationSize.QuadPart = 0;
            else if (atts & FILE_ATTRIBUTE_SPARSE_FILE)
                fibdi->AllocationSize.QuadPart = ii.st_blocks;
            else
                fibdi->AllocationSize.QuadPart = sector_align(ii.st_size, fcb->Vcb->superblock.sector_size);

            fibdi->FileAttributes = atts;
            fibdi->FileNameLength = de->name.Length;
            fibdi->EaSize = (r && atts & FILE_ATTRIBUTE_REPARSE_POINT) ? get_reparse_tag(fcb->Vcb, r, inode, de->type, atts, ccb->lxss, Irp) : ealen;
            fibdi->ShortNameLength = 0;
            fibdi->FileId.QuadPart = r ? make_file_id(r, inode) : make_file_id(fcb->Vcb->dummy_fcb->subvol, fcb->Vcb->dummy_fcb->inode);

            RtlCopyMemory(fibdi->FileName, de->name.Buffer, de->name.Length);

            *len -= needed;

            return STATUS_SUCCESS;
        }

        case FileIdFullDirectoryInformation:
        {
            FILE_ID_FULL_DIR_INFORMATION* fifdi = buf;

            TRACE("FileIdFullDirectoryInformation\n");

            needed = sizeof(FILE_ID_FULL_DIR_INFORMATION) - sizeof(WCHAR) + de->name.Length;

            if (needed > *len) {
                TRACE("buffer overflow - %li > %lu\n", needed, *len);
                return STATUS_BUFFER_OVERFLOW;
            }

            fifdi->NextEntryOffset = 0;
            fifdi->FileIndex = 0;
            fifdi->CreationTime.QuadPart = unix_time_to_win(&ii.otime);
            fifdi->LastAccessTime.QuadPart = unix_time_to_win(&ii.st_atime);
            fifdi->LastWriteTime.QuadPart = unix_time_to_win(&ii.st_mtime);
            fifdi->ChangeTime.QuadPart = unix_time_to_win(&ii.st_ctime);
            fifdi->EndOfFile.QuadPart = de->type == BTRFS_TYPE_SYMLINK ? 0 : ii.st_size;

            if (de->type == BTRFS_TYPE_SYMLINK)
                fifdi->AllocationSize.QuadPart = 0;
            else if (atts & FILE_ATTRIBUTE_SPARSE_FILE)
                fifdi->AllocationSize.QuadPart = ii.st_blocks;
            else
                fifdi->AllocationSize.QuadPart = sector_align(ii.st_size, fcb->Vcb->superblock.sector_size);

            fifdi->FileAttributes = atts;
            fifdi->FileNameLength = de->name.Length;
            fifdi->EaSize = (r && atts & FILE_ATTRIBUTE_REPARSE_POINT) ? get_reparse_tag(fcb->Vcb, r, inode, de->type, atts, ccb->lxss, Irp) : ealen;
            fifdi->FileId.QuadPart = r ? make_file_id(r, inode) : make_file_id(fcb->Vcb->dummy_fcb->subvol, fcb->Vcb->dummy_fcb->inode);

            RtlCopyMemory(fifdi->FileName, de->name.Buffer, de->name.Length);

            *len -= needed;

            return STATUS_SUCCESS;
        }

#ifndef _MSC_VER
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wswitch"
#endif
        case FileIdExtdDirectoryInformation:
        {
            FILE_ID_EXTD_DIR_INFORMATION* fiedi = buf;

            TRACE("FileIdExtdDirectoryInformation\n");

            needed = offsetof(FILE_ID_EXTD_DIR_INFORMATION, FileName[0]) + de->name.Length;

            if (needed > *len) {
                TRACE("buffer overflow - %li > %lu\n", needed, *len);
                return STATUS_BUFFER_OVERFLOW;
            }

            fiedi->NextEntryOffset = 0;
            fiedi->FileIndex = 0;
            fiedi->CreationTime.QuadPart = unix_time_to_win(&ii.otime);
            fiedi->LastAccessTime.QuadPart = unix_time_to_win(&ii.st_atime);
            fiedi->LastWriteTime.QuadPart = unix_time_to_win(&ii.st_mtime);
            fiedi->ChangeTime.QuadPart = unix_time_to_win(&ii.st_ctime);
            fiedi->EndOfFile.QuadPart = de->type == BTRFS_TYPE_SYMLINK ? 0 : ii.st_size;

            if (de->type == BTRFS_TYPE_SYMLINK)
                fiedi->AllocationSize.QuadPart = 0;
            else if (atts & FILE_ATTRIBUTE_SPARSE_FILE)
                fiedi->AllocationSize.QuadPart = ii.st_blocks;
            else
                fiedi->AllocationSize.QuadPart = sector_align(ii.st_size, fcb->Vcb->superblock.sector_size);

            fiedi->FileAttributes = atts;
            fiedi->FileNameLength = de->name.Length;
            fiedi->EaSize = ealen;
            fiedi->ReparsePointTag = get_reparse_tag(fcb->Vcb, r, inode, de->type, atts, ccb->lxss, Irp);

            RtlCopyMemory(&fiedi->FileId.Identifier[0], &fcb->inode, sizeof(uint64_t));
            RtlCopyMemory(&fiedi->FileId.Identifier[sizeof(uint64_t)], &fcb->subvol->id, sizeof(uint64_t));

            RtlCopyMemory(fiedi->FileName, de->name.Buffer, de->name.Length);

            *len -= needed;

            return STATUS_SUCCESS;
        }

        case FileIdExtdBothDirectoryInformation:
        {
            FILE_ID_EXTD_BOTH_DIR_INFORMATION* fiebdi = buf;

            TRACE("FileIdExtdBothDirectoryInformation\n");

            needed = offsetof(FILE_ID_EXTD_BOTH_DIR_INFORMATION, FileName[0]) + de->name.Length;

            if (needed > *len) {
                TRACE("buffer overflow - %li > %lu\n", needed, *len);
                return STATUS_BUFFER_OVERFLOW;
            }

            fiebdi->NextEntryOffset = 0;
            fiebdi->FileIndex = 0;
            fiebdi->CreationTime.QuadPart = unix_time_to_win(&ii.otime);
            fiebdi->LastAccessTime.QuadPart = unix_time_to_win(&ii.st_atime);
            fiebdi->LastWriteTime.QuadPart = unix_time_to_win(&ii.st_mtime);
            fiebdi->ChangeTime.QuadPart = unix_time_to_win(&ii.st_ctime);
            fiebdi->EndOfFile.QuadPart = de->type == BTRFS_TYPE_SYMLINK ? 0 : ii.st_size;

            if (de->type == BTRFS_TYPE_SYMLINK)
                fiebdi->AllocationSize.QuadPart = 0;
            else if (atts & FILE_ATTRIBUTE_SPARSE_FILE)
                fiebdi->AllocationSize.QuadPart = ii.st_blocks;
            else
                fiebdi->AllocationSize.QuadPart = sector_align(ii.st_size, fcb->Vcb->superblock.sector_size);

            fiebdi->FileAttributes = atts;
            fiebdi->FileNameLength = de->name.Length;
            fiebdi->EaSize = ealen;
            fiebdi->ReparsePointTag = get_reparse_tag(fcb->Vcb, r, inode, de->type, atts, ccb->lxss, Irp);

            RtlCopyMemory(&fiebdi->FileId.Identifier[0], &fcb->inode, sizeof(uint64_t));
            RtlCopyMemory(&fiebdi->FileId.Identifier[sizeof(uint64_t)], &fcb->subvol->id, sizeof(uint64_t));

            fiebdi->ShortNameLength = 0;

            RtlCopyMemory(fiebdi->FileName, de->name.Buffer, de->name.Length);

            *len -= needed;

            return STATUS_SUCCESS;
        }

#ifndef _MSC_VER
#pragma GCC diagnostic pop
#endif

        case FileNamesInformation:
        {
            FILE_NAMES_INFORMATION* fni = buf;

            TRACE("FileNamesInformation\n");

            needed = sizeof(FILE_NAMES_INFORMATION) - sizeof(WCHAR) + de->name.Length;

            if (needed > *len) {
                TRACE("buffer overflow - %li > %lu\n", needed, *len);
                return STATUS_BUFFER_OVERFLOW;
            }

            fni->NextEntryOffset = 0;
            fni->FileIndex = 0;
            fni->FileNameLength = de->name.Length;

            RtlCopyMemory(fni->FileName, de->name.Buffer, de->name.Length);

            *len -= needed;

            return STATUS_SUCCESS;
        }

        case FileObjectIdInformation:
            FIXME("STUB: FileObjectIdInformation\n");
            return STATUS_NOT_IMPLEMENTED;

        case FileQuotaInformation:
            FIXME("STUB: FileQuotaInformation\n");
            return STATUS_NOT_IMPLEMENTED;

        case FileReparsePointInformation:
            FIXME("STUB: FileReparsePointInformation\n");
            return STATUS_NOT_IMPLEMENTED;

        default:
            WARN("Unknown FileInformationClass %u\n", IrpSp->Parameters.QueryDirectory.FileInformationClass);
            return STATUS_NOT_IMPLEMENTED;
    }

    return STATUS_NO_MORE_FILES;
}

static NTSTATUS next_dir_entry(file_ref* fileref, uint64_t* offset, dir_entry* de, dir_child** pdc) {
    LIST_ENTRY* le;
    dir_child* dc;

    if (*pdc) {
        dir_child* dc2 = *pdc;

        if (dc2->list_entry_index.Flink != &fileref->fcb->dir_children_index)
            dc = CONTAINING_RECORD(dc2->list_entry_index.Flink, dir_child, list_entry_index);
        else
            dc = NULL;

        goto next;
    }

    if (fileref->parent) { // don't return . and .. if root directory
        if (*offset == 0) {
            de->key.obj_id = fileref->fcb->inode;
            de->key.obj_type = TYPE_INODE_ITEM;
            de->key.offset = 0;
            de->dir_entry_type = DirEntryType_Self;
            de->name.Buffer = L".";
            de->name.Length = de->name.MaximumLength = sizeof(WCHAR);
            de->type = BTRFS_TYPE_DIRECTORY;

            *offset = 1;
            *pdc = NULL;

            return STATUS_SUCCESS;
        } else if (*offset == 1) {
            de->key.obj_id = fileref->parent->fcb->inode;
            de->key.obj_type = TYPE_INODE_ITEM;
            de->key.offset = 0;
            de->dir_entry_type = DirEntryType_Parent;
            de->name.Buffer = L"..";
            de->name.Length = de->name.MaximumLength = sizeof(WCHAR) * 2;
            de->type = BTRFS_TYPE_DIRECTORY;

            *offset = 2;
            *pdc = NULL;

            return STATUS_SUCCESS;
        }
    }

    if (*offset < 2)
        *offset = 2;

    dc = NULL;
    le = fileref->fcb->dir_children_index.Flink;

    // skip entries before offset
    while (le != &fileref->fcb->dir_children_index) {
        dir_child* dc2 = CONTAINING_RECORD(le, dir_child, list_entry_index);

        if (dc2->index >= *offset) {
            dc = dc2;
            break;
        }

        le = le->Flink;
    }

next:
    if (!dc)
        return STATUS_NO_MORE_FILES;

    de->key = dc->key;
    de->name = dc->name;
    de->type = dc->type;
    de->dir_entry_type = DirEntryType_File;
    de->dc = dc;

    *offset = dc->index + 1;
    *pdc = dc;

    return STATUS_SUCCESS;
}

static NTSTATUS query_directory(PIRP Irp) {
    PIO_STACK_LOCATION IrpSp;
    NTSTATUS Status, status2;
    fcb* fcb;
    ccb* ccb;
    file_ref* fileref;
    device_extension* Vcb;
    void* buf;
    uint8_t *curitem, *lastitem;
    LONG length;
    ULONG count;
    bool has_wildcard = false, specific_file = false, initial;
    dir_entry de;
    uint64_t newoffset;
    dir_child* dc = NULL;

    TRACE("query directory\n");

    IrpSp = IoGetCurrentIrpStackLocation(Irp);
    fcb = IrpSp->FileObject->FsContext;
    ccb = IrpSp->FileObject->FsContext2;
    fileref = ccb ? ccb->fileref : NULL;

    if (!fileref)
        return STATUS_INVALID_PARAMETER;

    if (!ccb) {
        ERR("ccb was NULL\n");
        return STATUS_INVALID_PARAMETER;
    }

    if (!fcb) {
        ERR("fcb was NULL\n");
        return STATUS_INVALID_PARAMETER;
    }

    if (Irp->RequestorMode == UserMode && !(ccb->access & FILE_LIST_DIRECTORY)) {
        WARN("insufficient privileges\n");
        return STATUS_ACCESS_DENIED;
    }

    Vcb = fcb->Vcb;

    if (!Vcb) {
        ERR("Vcb was NULL\n");
        return STATUS_INVALID_PARAMETER;
    }

    if (fileref->fcb == Vcb->dummy_fcb)
        return STATUS_NO_MORE_FILES;

    if (IrpSp->Flags == 0) {
        TRACE("QD flags: (none)\n");
    } else {
        ULONG flags = IrpSp->Flags;

        TRACE("QD flags:\n");

        if (flags & SL_INDEX_SPECIFIED) {
            TRACE("    SL_INDEX_SPECIFIED\n");
            flags &= ~SL_INDEX_SPECIFIED;
        }

        if (flags & SL_RESTART_SCAN) {
            TRACE("    SL_RESTART_SCAN\n");
            flags &= ~SL_RESTART_SCAN;
        }

        if (flags & SL_RETURN_SINGLE_ENTRY) {
            TRACE("    SL_RETURN_SINGLE_ENTRY\n");
            flags &= ~SL_RETURN_SINGLE_ENTRY;
        }

        if (flags != 0)
            TRACE("    unknown flags: %lu\n", flags);
    }

    if (IrpSp->Flags & SL_RESTART_SCAN) {
        ccb->query_dir_offset = 0;

        if (ccb->query_string.Buffer) {
            RtlFreeUnicodeString(&ccb->query_string);
            ccb->query_string.Buffer = NULL;
        }

        ccb->has_wildcard = false;
        ccb->specific_file = false;
    }

    initial = !ccb->query_string.Buffer;

    if (IrpSp->Parameters.QueryDirectory.FileName && IrpSp->Parameters.QueryDirectory.FileName->Length > 1) {
        TRACE("QD filename: %.*S\n", (int)(IrpSp->Parameters.QueryDirectory.FileName->Length / sizeof(WCHAR)), IrpSp->Parameters.QueryDirectory.FileName->Buffer);

        if (IrpSp->Parameters.QueryDirectory.FileName->Length > sizeof(WCHAR) || IrpSp->Parameters.QueryDirectory.FileName->Buffer[0] != L'*') {
            specific_file = true;

            if (FsRtlDoesNameContainWildCards(IrpSp->Parameters.QueryDirectory.FileName)) {
                has_wildcard = true;
                specific_file = false;
            }
        }

        if (ccb->query_string.Buffer)
            RtlFreeUnicodeString(&ccb->query_string);

        if (has_wildcard)
            RtlUpcaseUnicodeString(&ccb->query_string, IrpSp->Parameters.QueryDirectory.FileName, true);
        else {
            ccb->query_string.Buffer = ExAllocatePoolWithTag(PagedPool, IrpSp->Parameters.QueryDirectory.FileName->Length, ALLOC_TAG);
            if (!ccb->query_string.Buffer) {
                ERR("out of memory\n");
                return STATUS_INSUFFICIENT_RESOURCES;
            }

            ccb->query_string.Length = ccb->query_string.MaximumLength = IrpSp->Parameters.QueryDirectory.FileName->Length;
            RtlCopyMemory(ccb->query_string.Buffer, IrpSp->Parameters.QueryDirectory.FileName->Buffer, IrpSp->Parameters.QueryDirectory.FileName->Length);
        }

        ccb->has_wildcard = has_wildcard;
        ccb->specific_file = specific_file;
    } else {
        has_wildcard = ccb->has_wildcard;
        specific_file = ccb->specific_file;

        if (!(IrpSp->Flags & SL_RESTART_SCAN)) {
            initial = false;

            if (specific_file)
                return STATUS_NO_MORE_FILES;
        }
    }

    if (ccb->query_string.Buffer) {
        TRACE("query string = %.*S\n", (int)(ccb->query_string.Length / sizeof(WCHAR)), ccb->query_string.Buffer);
    }

    newoffset = ccb->query_dir_offset;

    ExAcquireResourceSharedLite(&Vcb->tree_lock, true);

    ExAcquireResourceSharedLite(&fileref->fcb->nonpaged->dir_children_lock, true);

    Status = next_dir_entry(fileref, &newoffset, &de, &dc);

    if (!NT_SUCCESS(Status)) {
        if (Status == STATUS_NO_MORE_FILES && initial)
            Status = STATUS_NO_SUCH_FILE;
        goto end;
    }

    ccb->query_dir_offset = newoffset;

    buf = map_user_buffer(Irp, NormalPagePriority);

    if (Irp->MdlAddress && !buf) {
        ERR("MmGetSystemAddressForMdlSafe returned NULL\n");
        Status = STATUS_INSUFFICIENT_RESOURCES;
        goto end;
    }

    length = IrpSp->Parameters.QueryDirectory.Length;

    if (specific_file) {
        bool found = false;
        UNICODE_STRING us;
        LIST_ENTRY* le;
        uint32_t hash;
        uint8_t c;

        us.Buffer = NULL;

        if (!ccb->case_sensitive) {
            Status = RtlUpcaseUnicodeString(&us, &ccb->query_string, true);
            if (!NT_SUCCESS(Status)) {
                ERR("RtlUpcaseUnicodeString returned %08lx\n", Status);
                goto end;
            }

            hash = calc_crc32c(0xffffffff, (uint8_t*)us.Buffer, us.Length);
        } else
            hash = calc_crc32c(0xffffffff, (uint8_t*)ccb->query_string.Buffer, ccb->query_string.Length);

        c = hash >> 24;

        if (ccb->case_sensitive) {
            if (fileref->fcb->hash_ptrs[c]) {
                le = fileref->fcb->hash_ptrs[c];
                while (le != &fileref->fcb->dir_children_hash) {
                    dir_child* dc2 = CONTAINING_RECORD(le, dir_child, list_entry_hash);

                    if (dc2->hash == hash) {
                        if (dc2->name.Length == ccb->query_string.Length && RtlCompareMemory(dc2->name.Buffer, ccb->query_string.Buffer, ccb->query_string.Length) == ccb->query_string.Length) {
                            found = true;

                            de.key = dc2->key;
                            de.name = dc2->name;
                            de.type = dc2->type;
                            de.dir_entry_type = DirEntryType_File;
                            de.dc = dc2;

                            break;
                        }
                    } else if (dc2->hash > hash)
                        break;

                    le = le->Flink;
                }
            }
        } else {
            if (fileref->fcb->hash_ptrs_uc[c]) {
                le = fileref->fcb->hash_ptrs_uc[c];
                while (le != &fileref->fcb->dir_children_hash_uc) {
                    dir_child* dc2 = CONTAINING_RECORD(le, dir_child, list_entry_hash_uc);

                    if (dc2->hash_uc == hash) {
                        if (dc2->name_uc.Length == us.Length && RtlCompareMemory(dc2->name_uc.Buffer, us.Buffer, us.Length) == us.Length) {
                            found = true;

                            de.key = dc2->key;
                            de.name = dc2->name;
                            de.type = dc2->type;
                            de.dir_entry_type = DirEntryType_File;
                            de.dc = dc2;

                            break;
                        }
                    } else if (dc2->hash_uc > hash)
                        break;

                    le = le->Flink;
                }
            }
        }

        if (us.Buffer)
            ExFreePool(us.Buffer);

        if (!found) {
            Status = STATUS_NO_SUCH_FILE;
            goto end;
        }
    } else if (has_wildcard) {
        while (!FsRtlIsNameInExpression(&ccb->query_string, &de.name, !ccb->case_sensitive, NULL)) {
            newoffset = ccb->query_dir_offset;
            Status = next_dir_entry(fileref, &newoffset, &de, &dc);

            if (NT_SUCCESS(Status))
                ccb->query_dir_offset = newoffset;
            else {
                if (Status == STATUS_NO_MORE_FILES && initial)
                    Status = STATUS_NO_SUCH_FILE;

                goto end;
            }
        }
    }

    TRACE("file(0) = %.*S\n", (int)(de.name.Length / sizeof(WCHAR)), de.name.Buffer);
    TRACE("offset = %I64u\n", ccb->query_dir_offset - 1);

    Status = query_dir_item(fcb, ccb, buf, &length, Irp, &de, fcb->subvol);

    count = 0;
    if (NT_SUCCESS(Status) && !(IrpSp->Flags & SL_RETURN_SINGLE_ENTRY) && !specific_file) {
        lastitem = (uint8_t*)buf;

        while (length > 0) {
            switch (IrpSp->Parameters.QueryDirectory.FileInformationClass) {
#ifndef _MSC_VER
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wswitch"
#endif
                case FileBothDirectoryInformation:
                case FileDirectoryInformation:
                case FileIdBothDirectoryInformation:
                case FileFullDirectoryInformation:
                case FileIdFullDirectoryInformation:
                case FileIdExtdDirectoryInformation:
                case FileIdExtdBothDirectoryInformation:
                    length -= length % 8;
                    break;
#ifndef _MSC_VER
#pragma GCC diagnostic pop
#endif

                case FileNamesInformation:
                    length -= length % 4;
                    break;

                default:
                    WARN("unhandled file information class %u\n", IrpSp->Parameters.QueryDirectory.FileInformationClass);
                    break;
            }

            if (length > 0) {
                newoffset = ccb->query_dir_offset;
                Status = next_dir_entry(fileref, &newoffset, &de, &dc);
                if (NT_SUCCESS(Status)) {
                    if (!has_wildcard || FsRtlIsNameInExpression(&ccb->query_string, &de.name, !ccb->case_sensitive, NULL)) {
                        curitem = (uint8_t*)buf + IrpSp->Parameters.QueryDirectory.Length - length;
                        count++;

                        TRACE("file(%lu) %Iu = %.*S\n", count, curitem - (uint8_t*)buf, (int)(de.name.Length / sizeof(WCHAR)), de.name.Buffer);
                        TRACE("offset = %I64u\n", ccb->query_dir_offset - 1);

                        status2 = query_dir_item(fcb, ccb, curitem, &length, Irp, &de, fcb->subvol);

                        if (NT_SUCCESS(status2)) {
                            ULONG* lastoffset = (ULONG*)lastitem;

                            *lastoffset = (ULONG)(curitem - lastitem);
                            ccb->query_dir_offset = newoffset;

                            lastitem = curitem;
                        } else
                            break;
                    } else
                        ccb->query_dir_offset = newoffset;
                } else {
                    if (Status == STATUS_NO_MORE_FILES)
                        Status = STATUS_SUCCESS;

                    break;
                }
            } else
                break;
        }
    }

    Irp->IoStatus.Information = IrpSp->Parameters.QueryDirectory.Length - length;

end:
    ExReleaseResourceLite(&fileref->fcb->nonpaged->dir_children_lock);

    ExReleaseResourceLite(&Vcb->tree_lock);

    TRACE("returning %08lx\n", Status);

    return Status;
}

static NTSTATUS notify_change_directory(device_extension* Vcb, PIRP Irp) {
    PIO_STACK_LOCATION IrpSp = IoGetCurrentIrpStackLocation(Irp);
    PFILE_OBJECT FileObject = IrpSp->FileObject;
    fcb* fcb = FileObject->FsContext;
    ccb* ccb = FileObject->FsContext2;
    file_ref* fileref = ccb ? ccb->fileref : NULL;
    NTSTATUS Status;

    TRACE("IRP_MN_NOTIFY_CHANGE_DIRECTORY\n");

    if (!ccb) {
        ERR("ccb was NULL\n");
        return STATUS_INVALID_PARAMETER;
    }

    if (!fileref) {
        ERR("no fileref\n");
        return STATUS_INVALID_PARAMETER;
    }

    if (Irp->RequestorMode == UserMode && !(ccb->access & FILE_LIST_DIRECTORY)) {
        WARN("insufficient privileges\n");
        return STATUS_ACCESS_DENIED;
    }

    ExAcquireResourceSharedLite(&fcb->Vcb->tree_lock, true);
    ExAcquireResourceExclusiveLite(fcb->Header.Resource, true);

    if (fcb->type != BTRFS_TYPE_DIRECTORY) {
        Status = STATUS_INVALID_PARAMETER;
        goto end;
    }

    // FIXME - raise exception if FCB marked for deletion?

    TRACE("FileObject %p\n", FileObject);

    if (ccb->filename.Length == 0) {
        ULONG reqlen;

        ccb->filename.MaximumLength = ccb->filename.Length = 0;

        Status = fileref_get_filename(fileref, &ccb->filename, NULL, &reqlen);
        if (Status == STATUS_BUFFER_OVERFLOW) {
            ccb->filename.Buffer = ExAllocatePoolWithTag(PagedPool, reqlen, ALLOC_TAG);
            if (!ccb->filename.Buffer) {
                ERR("out of memory\n");
                Status = STATUS_INSUFFICIENT_RESOURCES;
                goto end;
            }

            ccb->filename.MaximumLength = (uint16_t)reqlen;

            Status = fileref_get_filename(fileref, &ccb->filename, NULL, &reqlen);
            if (!NT_SUCCESS(Status)) {
                ERR("fileref_get_filename returned %08lx\n", Status);
                goto end;
            }
        } else {
            ERR("fileref_get_filename returned %08lx\n", Status);
            goto end;
        }
    }

    FsRtlNotifyFilterChangeDirectory(Vcb->NotifySync, &Vcb->DirNotifyList, FileObject->FsContext2, (PSTRING)&ccb->filename,
                                     IrpSp->Flags & SL_WATCH_TREE, false, IrpSp->Parameters.NotifyDirectory.CompletionFilter, Irp,
                                     NULL, NULL, NULL);

    Status = STATUS_PENDING;

end:
    ExReleaseResourceLite(fcb->Header.Resource);
    ExReleaseResourceLite(&fcb->Vcb->tree_lock);

    return Status;
}

_Dispatch_type_(IRP_MJ_DIRECTORY_CONTROL)
_Function_class_(DRIVER_DISPATCH)
NTSTATUS __stdcall drv_directory_control(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) {
    PIO_STACK_LOCATION IrpSp;
    NTSTATUS Status;
    ULONG func;
    bool top_level;
    device_extension* Vcb = DeviceObject->DeviceExtension;

    FsRtlEnterFileSystem();

    TRACE("directory control\n");

    top_level = is_top_level(Irp);

    if (Vcb && Vcb->type == VCB_TYPE_VOLUME) {
        Status = vol_directory_control(DeviceObject, Irp);
        goto end;
    } else if (!Vcb || Vcb->type != VCB_TYPE_FS) {
        Status = STATUS_INVALID_PARAMETER;
        goto end;
    }

    IrpSp = IoGetCurrentIrpStackLocation(Irp);

    Irp->IoStatus.Information = 0;

    func = IrpSp->MinorFunction;

    switch (func) {
        case IRP_MN_NOTIFY_CHANGE_DIRECTORY:
            Status = notify_change_directory(Vcb, Irp);
            break;

        case IRP_MN_QUERY_DIRECTORY:
            Status = query_directory(Irp);
            break;

        default:
            WARN("unknown minor %lu\n", func);
            Status = STATUS_NOT_IMPLEMENTED;
            Irp->IoStatus.Status = Status;
            break;
    }

    if (Status == STATUS_PENDING)
        goto exit;

end:
    Irp->IoStatus.Status = Status;

    IoCompleteRequest(Irp, IO_DISK_INCREMENT);

exit:
    TRACE("returning %08lx\n", Status);

    if (top_level)
        IoSetTopLevelIrp(NULL);

    FsRtlExitFileSystem();

    return Status;
}
