<|start_filename|>slmenu/slmenu.c<|end_filename|>
/**
 * @file slmenu/slmenu.c
 *
 * Yori shell single line menu
 *
 * Copyright (c) 2018-2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>
#include <yoricall.h>
#include <yoriwin.h>

/**
 Help text to display to the user.
 */
const
CHAR strSlmenuHelpText[] =
        "\n"
        "Displays a menu based on standard input and displays selection to output."
        "\n"
        "SLMENU [-license] [-b|-t|-l <lines>] [-p <text>]\n"
        "\n"
        "   -b             Display single line at the bottom of the window\n"
        "   -l <lines>     Display in multiple lines and specify the number of lines\n" 
        "   -p <text>      Display prompt text before items\n" 
        "   -t             Display single line at the top of the window\n";

/**
 Display usage text to the user.
 */
BOOL
SlmenuHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Slmenu %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strSlmenuHelpText);
    return TRUE;
}

/**
 Context information passed between reading the input stream (including all
 lines to populate) and menu display.
 */
typedef struct _SLMENU_CONTEXT {

    /**
     An array of strings to populate into to the menu.
     */
    PYORI_STRING StringArray;

    /**
     The number of elements allocated in StringArray.
     */
    DWORD StringsAllocated;

    /**
     The number of strings populated in the StringArray.
     */
    DWORD StringCount;
} SLMENU_CONTEXT, *PSLMENU_CONTEXT;

/**
 A callback invoked when the ok button is clicked.

 @param Ctrl Pointer to the button that was clicked.
 */
VOID
SlmenuOkButtonClicked(
    __in PYORI_WIN_CTRL_HANDLE Ctrl
    )
{
    PYORI_WIN_CTRL_HANDLE Parent;
    Parent = YoriWinGetControlParent(Ctrl);
    YoriWinCloseWindow(Parent, TRUE);
}

/**
 A callback invoked when the cancel button is clicked.

 @param Ctrl Pointer to the button that was clicked.
 */
VOID
SlmenuCancelButtonClicked(
    __in PYORI_WIN_CTRL_HANDLE Ctrl
    )
{
    PYORI_WIN_CTRL_HANDLE Parent;
    Parent = YoriWinGetControlParent(Ctrl);
    YoriWinCloseWindow(Parent, FALSE);
}

/**
 The set of locations where a single line menu can be displayed.
 */
typedef enum _SLMENU_LOCATION {
    SlmenuCurrentLine = 0,
    SlmenuTopLine = 1,
    SlmenuBottomLine = 2,
    SlmenuCurrentLineRemainder = 3
} SLMENU_LOCATION;

/**
 Given an array of items and a control width, calculate the "best" size for
 each item.  If everything fits, this is the same as the longest item.  If
 it doesn't fit, truncate up to twice the average size.  Recalculate the
 width based on the actual control size to ensure that any extra cells are
 allocated to each item as much as possible.

 @param MenuOptions Pointer to an array of item strings.

 @param NumberOptions The number of strings.

 @param ControlWidth The width of the list control, in characters.

 @return The width in characters of each element.
 */
WORD
SlmenuCalculateColumnWidthForItems(
    __in PYORI_STRING MenuOptions,
    __in DWORD NumberOptions,
    __in DWORD ControlWidth
    )
{
    DWORD LongestItem;
    DWORD AverageNameLength;
    DWORD TotalLength;
    DWORD ColumnWidth;
    DWORD ColumnCount;

    DWORD Index;

    TotalLength = 0;
    LongestItem = 0;
    for (Index = 0; Index < NumberOptions; Index++) {
        TotalLength = TotalLength + MenuOptions[Index].LengthInChars;
        if (MenuOptions[Index].LengthInChars > LongestItem) {
            LongestItem = MenuOptions[Index].LengthInChars;
        }
    }

    AverageNameLength = TotalLength / NumberOptions;

    //
    //  The item width contains two padding characters, so add those here.
    //

    ColumnWidth = LongestItem + 2;

    if (ColumnWidth * NumberOptions > ControlWidth) {
        if (LongestItem > 2 * AverageNameLength) {
            ColumnWidth = 2 * AverageNameLength;
        }
    }

    ColumnCount = ControlWidth / ColumnWidth;
    if (ColumnCount == 0) {
        ColumnCount = 1;
    }

    ColumnWidth = ControlWidth / ColumnCount;
    return (WORD)ColumnWidth;
}

/**
 Display a popup window containing a list of items.

 @param MenuOptions Pointer to an array of list items.

 @param NumberOptions The number of elements within the array.

 @param Location The location to display the line.

 @param Title The string to display on the title bar of the dialog.

 @param ActiveOption On successful completion, updated to contain the option
        that the user selected.

 @return TRUE to indicate that the user successfully selected an option, FALSE
         to indicate the menu could not be displayed or the user cancelled the
         operation.
 */
__success(return)
BOOL
SlmenuCreateSinglelineMenu(
    __in PYORI_STRING MenuOptions,
    __in DWORD NumberOptions,
    __in SLMENU_LOCATION Location,
    __in_opt PYORI_STRING Title,
    __out PDWORD ActiveOption
    )
{
    PYORI_WIN_WINDOW_MANAGER_HANDLE WinMgr;
    PYORI_WIN_CTRL_HANDLE List;
    PYORI_WIN_CTRL_HANDLE Label;
    PYORI_WIN_WINDOW_HANDLE Parent;
    SMALL_RECT CtrlRect;
    COORD WindowSize;
    YORI_STRING Caption;
    WORD ButtonWidth;
    PYORI_WIN_CTRL_HANDLE Ctrl;
    DWORD_PTR Result;
    SMALL_RECT WinMgrPos;

    if (NumberOptions == 0) {
        return FALSE;
    }

    if (!YoriWinOpenWindowManager(FALSE, &WinMgr)) {
        return FALSE;
    }

    if (!YoriWinGetWinMgrLocation(WinMgr, &WinMgrPos)) {
        YoriWinCloseWindowManager(WinMgr);
        return FALSE;
    }

    CtrlRect.Left = WinMgrPos.Left;
    CtrlRect.Right = WinMgrPos.Right;
    CtrlRect.Top = WinMgrPos.Top;
    CtrlRect.Bottom = WinMgrPos.Top;

    if (Location == SlmenuBottomLine) {
        CtrlRect.Top = WinMgrPos.Bottom;
        CtrlRect.Bottom = WinMgrPos.Bottom;
    } else if (Location == SlmenuCurrentLine ||
               Location == SlmenuCurrentLineRemainder) {
        COORD CursorLocation;
        if (!YoriWinGetWinMgrInitialCursorLocation(WinMgr, &CursorLocation)) {
            YoriWinCloseWindowManager(WinMgr);
            return FALSE;
        }

        CtrlRect.Top = CursorLocation.Y;
        CtrlRect.Bottom = CursorLocation.Y;

        if (Location == SlmenuCurrentLineRemainder) {
            CtrlRect.Left = CursorLocation.X;
        }
    }

    if (!YoriWinCreateWindowEx(WinMgr, &CtrlRect, 0, NULL, &Parent)) {
        YoriWinCloseWindowManager(WinMgr);
        return FALSE;
    }

    YoriWinGetClientSize(Parent, &WindowSize);

    CtrlRect.Left = 0;
    CtrlRect.Top = 0;
    CtrlRect.Bottom = 0;

    if (Title != NULL) {
        CtrlRect.Left = 0;
        CtrlRect.Top = 0;
        CtrlRect.Bottom = 0;
        CtrlRect.Right = (WORD)(Title->LengthInChars - 1);

        Label = YoriWinLabelCreate(Parent, &CtrlRect, Title, 0);
        CtrlRect.Left = (WORD)(CtrlRect.Right + 1);
    }

    CtrlRect.Right = (SHORT)(WindowSize.X - 1);

    List = YoriWinListCreate(Parent, &CtrlRect, YORI_WIN_LIST_STYLE_HORIZONTAL | YORI_WIN_LIST_STYLE_NO_BORDER);
    if (List == NULL) {
        YoriWinDestroyWindow(Parent);
        YoriWinCloseWindowManager(WinMgr);
        return FALSE;
    }

    YoriWinGetClientSize(List, &WindowSize);
    ButtonWidth = SlmenuCalculateColumnWidthForItems(MenuOptions, NumberOptions, WindowSize.X);
    YoriWinListSetHorizontalItemWidth(List, ButtonWidth);

    if (!YoriWinListAddItems(List, MenuOptions, NumberOptions)) {
        YoriWinDestroyWindow(Parent);
        YoriWinCloseWindowManager(WinMgr);
        return FALSE;
    }

    YoriWinListSetActiveOption(List, 0);

    ButtonWidth = (WORD)(sizeof("Cancel") - 1 + 2);

    CtrlRect.Top = (SHORT)(WindowSize.Y - 3);
    CtrlRect.Bottom = (SHORT)(CtrlRect.Top + 2);

    YoriLibConstantString(&Caption, _T("&Ok"));

    //
    //  WindowSize corresponds to dimensions, so rightmost cell is one
    //  less.  The button starts two buttons over, and each button
    //  has its client plus border chars, and there's an extra char
    //  between the buttons.
    //

    CtrlRect.Left = (SHORT)(WindowSize.X - 1 - 2 * (ButtonWidth + 2) - 1);
    CtrlRect.Right = (WORD)(CtrlRect.Left + ButtonWidth + 1);

    Ctrl = YoriWinButtonCreate(Parent, &CtrlRect, &Caption, YORI_WIN_BUTTON_STYLE_DEFAULT | YORI_WIN_BUTTON_STYLE_DISABLE_FOCUS, SlmenuOkButtonClicked);
    if (Ctrl == NULL) {
        YoriWinDestroyWindow(Parent);
        YoriWinCloseWindowManager(WinMgr);
        return FALSE;
    }

    YoriLibConstantString(&Caption, _T("&Cancel"));

    CtrlRect.Left = (SHORT)(WindowSize.X - 1 - (ButtonWidth + 2));
    CtrlRect.Right = (WORD)(CtrlRect.Left + ButtonWidth + 1);

    Ctrl = YoriWinButtonCreate(Parent, &CtrlRect, &Caption, YORI_WIN_BUTTON_STYLE_CANCEL | YORI_WIN_BUTTON_STYLE_DISABLE_FOCUS, SlmenuCancelButtonClicked);
    if (Ctrl == NULL) {
        YoriWinDestroyWindow(Parent);
        YoriWinCloseWindowManager(WinMgr);
        return FALSE;
    }

    Result = FALSE;
    if (!YoriWinProcessInputForWindow(Parent, &Result)) {
        Result = FALSE;
    }
    if (Result) {
        if (!YoriWinListGetActiveOption(List, ActiveOption)) {
            Result = FALSE;
        }
    }

    YoriWinDestroyWindow(Parent);
    YoriWinCloseWindowManager(WinMgr);
    return (BOOL)Result;
}

/**
 Display a popup window containing a list of items.

 @param MenuOptions Pointer to an array of list items.

 @param NumberOptions The number of elements within the array.

 @param Title The string to display on the title bar of the dialog.

 @param DisplayLineCount The number of lines of options to display.  The
        dialog adds overhead on top of this count.

 @param ActiveOption On successful completion, updated to contain the option
        that the user selected.

 @return TRUE to indicate that the user successfully selected an option, FALSE
         to indicate the menu could not be displayed or the user cancelled the
         operation.
 */
__success(return)
BOOL
SlmenuCreateMultilineMenu(
    __in PYORI_STRING MenuOptions,
    __in DWORD NumberOptions,
    __in PYORI_STRING Title,
    __in DWORD DisplayLineCount,
    __out PDWORD ActiveOption
    )
{
    PYORI_WIN_WINDOW_MANAGER_HANDLE WinMgr;
    PYORI_WIN_CTRL_HANDLE List;
    PYORI_WIN_WINDOW_HANDLE Parent;
    SMALL_RECT ListRect;
    COORD WindowSize;
    SMALL_RECT ButtonArea;
    YORI_STRING Caption;
    WORD ButtonWidth;
    PYORI_WIN_CTRL_HANDLE Ctrl;
    DWORD_PTR Result;
    WORD WindowHeight;

    if (NumberOptions == 0) {
        return FALSE;
    }

    if (!YoriWinOpenWindowManager(FALSE, &WinMgr)) {
        return FALSE;
    }

    WindowHeight = (WORD)(DisplayLineCount + 9);

    if (!YoriWinCreateWindow(WinMgr, 30, WindowHeight, 60, WindowHeight, YORI_WIN_WINDOW_STYLE_BORDER_SINGLE | YORI_WIN_WINDOW_STYLE_SHADOW_SOLID, Title, &Parent)) {
        YoriWinCloseWindowManager(WinMgr);
        return FALSE;
    }

    YoriWinGetClientSize(Parent, &WindowSize);

    ListRect.Left = 1;
    ListRect.Top = 1;
    ListRect.Right = (SHORT)(WindowSize.X - 2);
    ListRect.Bottom = (SHORT)(WindowSize.Y - 3 - 1);

    List = YoriWinListCreate(Parent, &ListRect, YORI_WIN_LIST_STYLE_VSCROLLBAR);
    if (List == NULL) {
        YoriWinDestroyWindow(Parent);
        YoriWinCloseWindowManager(WinMgr);
        return FALSE;
    }

    if (!YoriWinListAddItems(List, MenuOptions, NumberOptions)) {
        YoriWinDestroyWindow(Parent);
        YoriWinCloseWindowManager(WinMgr);
        return FALSE;
    }

    YoriWinListSetActiveOption(List, 0);

    ButtonWidth = (WORD)(sizeof("Cancel") - 1 + 2);

    ButtonArea.Top = (SHORT)(WindowSize.Y - 3);
    ButtonArea.Bottom = (SHORT)(ButtonArea.Top + 2);

    YoriLibConstantString(&Caption, _T("&Ok"));

    //
    //  WindowSize corresponds to dimensions, so rightmost cell is one
    //  less.  The button starts two buttons over, and each button
    //  has its client plus border chars, and there's an extra char
    //  between the buttons.
    //

    ButtonArea.Left = (SHORT)(WindowSize.X - 1 - 2 * (ButtonWidth + 2) - 1);
    ButtonArea.Right = (WORD)(ButtonArea.Left + ButtonWidth + 1);

    Ctrl = YoriWinButtonCreate(Parent, &ButtonArea, &Caption, YORI_WIN_BUTTON_STYLE_DEFAULT, SlmenuOkButtonClicked);
    if (Ctrl == NULL) {
        YoriWinDestroyWindow(Parent);
        YoriWinCloseWindowManager(WinMgr);
        return FALSE;
    }

    YoriLibConstantString(&Caption, _T("&Cancel"));

    ButtonArea.Left = (SHORT)(WindowSize.X - 1 - (ButtonWidth + 2));
    ButtonArea.Right = (WORD)(ButtonArea.Left + ButtonWidth + 1);

    Ctrl = YoriWinButtonCreate(Parent, &ButtonArea, &Caption, YORI_WIN_BUTTON_STYLE_CANCEL, SlmenuCancelButtonClicked);
    if (Ctrl == NULL) {
        YoriWinDestroyWindow(Parent);
        YoriWinCloseWindowManager(WinMgr);
        return FALSE;
    }

    Result = FALSE;
    if (!YoriWinProcessInputForWindow(Parent, &Result)) {
        Result = FALSE;
    }
    if (Result) {
        if (!YoriWinListGetActiveOption(List, ActiveOption)) {
            Result = FALSE;
        }
    }

    YoriWinDestroyWindow(Parent);
    YoriWinCloseWindowManager(WinMgr);
    return (BOOL)Result;
}

/**
 Process a single opened stream, enumerating through all lines and loading
 them into an array.

 @param hSource The opened source stream.

 @param MenuContext Pointer to context information containing the lines.
 
 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SlmenuProcessStream(
    __in HANDLE hSource,
    __in PSLMENU_CONTEXT MenuContext
    )
{
    PVOID LineContext = NULL;
    DWORD Index;

    while (TRUE) {

        if (MenuContext->StringCount + 1 > MenuContext->StringsAllocated) {
            DWORD NewStringsAllocated;
            PYORI_STRING NewStrings;

            NewStringsAllocated = MenuContext->StringsAllocated * 4;
            if (NewStringsAllocated < 0x100) {
                NewStringsAllocated = 0x100;
            }

            NewStrings = YoriLibReferencedMalloc(NewStringsAllocated * sizeof(YORI_STRING));
            if (NewStrings == NULL) {
                return FALSE;
            }

            for (Index = MenuContext->StringCount; Index < NewStringsAllocated; Index++) {
                YoriLibInitEmptyString(&NewStrings[Index]);
            }

            if (MenuContext->StringCount > 0) {
                memcpy(NewStrings, MenuContext->StringArray, MenuContext->StringCount * sizeof(YORI_STRING));
            }
            if (MenuContext->StringArray != NULL) {
                YoriLibDereference(MenuContext->StringArray);
            }
            MenuContext->StringArray = NewStrings;
            MenuContext->StringsAllocated = NewStringsAllocated;
        }

        if (!YoriLibReadLineToString(&MenuContext->StringArray[MenuContext->StringCount], &LineContext, hSource)) {
            break;
        }

        MenuContext->StringCount++;
    }

    YoriLibLineReadCloseOrCache(LineContext);
    return TRUE;
}

/**
 Deallocate strings and the string array in the menu context.

 @param MenuContext Pointer to the context to clean up.
 */
VOID
SlmenuCleanupContext(
    __in PSLMENU_CONTEXT MenuContext
    )
{
    DWORD Index;

    for (Index = 0; Index < MenuContext->StringCount; Index++) {
        YoriLibFreeStringContents(&MenuContext->StringArray[Index]);
    }

    if (MenuContext->StringArray != NULL) {
        YoriLibDereference(MenuContext->StringArray);
    }
}

/**
 The set of operations this module is capable of performing.
 */
typedef enum _SLMENU_OP {
    SlmenuUnknown = 0,
    SlmenuDisplayMultilineMenu = 1,
    SlmenuDisplaySinglelineMenu = 2,
} SLMENU_OP;

#ifdef YORI_BUILTIN
/**
 The main entrypoint for the slmenu builtin command.
 */
#define ENTRYPOINT YoriCmd_SLMENU
#else
/**
 The main entrypoint for the slmenu standalone application.
 */
#define ENTRYPOINT ymain
#endif


/**
 Display selectable menu.

 @param ArgC The number of arguments.

 @param ArgV The argument array.

 @return ExitCode, zero for success, nonzero for failure.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    YORI_STRING Arg;
    SLMENU_OP Op;
    SLMENU_CONTEXT MenuContext;
    SLMENU_LOCATION Location;
    YORI_STRING Prompt;
    PYORI_STRING DisplayPrompt;
    DWORD DisplayLineCount;
    DWORD Index;
    DWORD Result;

    ZeroMemory(&MenuContext, sizeof(MenuContext));
    Op = SlmenuDisplaySinglelineMenu;
    DisplayLineCount = 0;
    Location = SlmenuCurrentLineRemainder;

    YoriLibConstantString(&Prompt, _T("Menu"));
    DisplayPrompt = NULL;

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                SlmenuHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2021"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("b")) == 0) {
                Location = SlmenuBottomLine;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("l")) == 0) {

                if (i + 1 < ArgC) {
                    LONGLONG llTemp;
                    DWORD CharsConsumed;
                    if (YoriLibStringToNumber(&ArgV[i + 1], TRUE, &llTemp, &CharsConsumed) &&
                        CharsConsumed > 0)  {

                        DisplayLineCount = (DWORD)llTemp;
                        Op = SlmenuDisplayMultilineMenu;
                        i++;
                        ArgumentUnderstood = TRUE;
                    }
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("p")) == 0) {
                if (i + 1 < ArgC) {
                    Prompt.StartOfString = ArgV[i + 1].StartOfString;
                    Prompt.LengthInChars = ArgV[i + 1].LengthInChars;
                    i++;
                    ArgumentUnderstood = TRUE;
                    DisplayPrompt = &Prompt;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("t")) == 0) {
                Location = SlmenuTopLine;
                ArgumentUnderstood = TRUE;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    Result = EXIT_SUCCESS;
    Index = 0;

    if (Op == SlmenuDisplaySinglelineMenu) {

        if (!SlmenuProcessStream(GetStdHandle(STD_INPUT_HANDLE), &MenuContext)) {
            Result = EXIT_FAILURE;
        }

        if (Result == EXIT_SUCCESS) {
            if (!SlmenuCreateSinglelineMenu(MenuContext.StringArray, MenuContext.StringCount, Location, DisplayPrompt, &Index)) {
                Result = EXIT_FAILURE;
            }
        }

        if (Result == EXIT_SUCCESS) {
            if (Index < MenuContext.StringCount) {
                YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%y"), &MenuContext.StringArray[Index]);
            }
        }

        SlmenuCleanupContext(&MenuContext);

    } else if (Op == SlmenuDisplayMultilineMenu) {

        if (!SlmenuProcessStream(GetStdHandle(STD_INPUT_HANDLE), &MenuContext)) {
            Result = EXIT_FAILURE;
        }

        if (Result == EXIT_SUCCESS) {
            if (!SlmenuCreateMultilineMenu(MenuContext.StringArray, MenuContext.StringCount, &Prompt, DisplayLineCount, &Index)) {
                Result = EXIT_FAILURE;
            }
        }

        if (Result == EXIT_SUCCESS) {
            if (Index < MenuContext.StringCount) {
                YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%y"), &MenuContext.StringArray[Index]);
            }
        }

        SlmenuCleanupContext(&MenuContext);
    }

#if !YORI_BUILTIN
    YoriLibLineReadCleanupCache();
#endif

    return Result;
}

// vim:sw=4:ts=4:et:


<|start_filename|>hexdump/hexdump.c<|end_filename|>
/**
 * @file hexdump/hexdump.c
 *
 * Yori shell display a file or files in hexadecimal form
 *
 * Copyright (c) 2017-2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

#pragma warning(disable: 4220) // Varargs matches remaining parameters

/**
 Help text to display to the user.
 */
const
CHAR strHexDumpHelpText[] =
        "\n"
        "Output the contents of one or more files in hex.\n"
        "\n"
        "HEXDUMP [-license] [-b] [-d] [-g1|-g2|-g4|-g8|-i] [-hc] [-ho]\n"
        "        [-l length] [-o offset] [-bin|-r] [-s] [-w] [<file>...]\n"
        "\n"
        "   -b             Use basic search criteria for files only\n"
        "   -bin           Process a stream of hex back into binary\n"
        "   -d             Display the differences between two files\n"
        "   -g             Number of bytes per display group\n"
        "   -hc            Hide character display\n"
        "   -ho            Hide offset within buffer\n"
        "   -i             C-style include output\n"
        "   -l             Length of the section to display\n"
        "   -o             Offset within the stream to display\n"
        "   -r             Reverse process hexdump hex back into binary\n"
        "   -s             Process files from all subdirectories\n"
        "   -w             Display wide (16 bit) characters\n";

/**
 Display usage text to the user.
 */
BOOL
HexDumpHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("HexDump %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strHexDumpHelpText);
    return TRUE;
}

/**
 Context passed to the callback which is invoked for each file found.
 */
typedef struct _HEXDUMP_CONTEXT {

    /**
     Records the total number of files processed.
     */
    LONGLONG FilesFound;

    /**
     Records the total number of files processed within a single command line
     argument.
     */
    LONGLONG FilesFoundThisArg;

    /**
     Offset within each stream to display.
     */
    LONGLONG OffsetToDisplay;

    /**
     Length within each stream to display.
     */
    LONGLONG LengthToDisplay;

    /**
     Number of bytes to display per group.
     */
    DWORD BytesPerGroup;

    /**
     The first error encountered when enumerating objects from a single arg.
     This is used to preserve file not found/path not found errors so that
     when the program falls back to interpreting the argument as a literal,
     if that still doesn't work, this is the error code that is displayed.
     */
    DWORD SavedErrorThisArg;

    /**
     If TRUE, hide the offset display within the buffer.
     */
    BOOLEAN HideOffset;

    /**
     If TRUE, hide the character display within the buffer.
     */
    BOOLEAN HideCharacters;

    /**
     If TRUE, display characters as wide.
     */
    BOOLEAN WideCharacters;

    /**
     If TRUE, output with C-style include output.
     */
    BOOLEAN CStyleInclude;

    /**
     TRUE if file enumeration is being performed recursively; FALSE if it is
     in one directory only.
     */
    BOOLEAN Recursive;

} HEXDUMP_CONTEXT, *PHEXDUMP_CONTEXT;

/**
 Check if the string starts with a consecutive section of hex characters.

 @param String Pointer to the string to check.

 @param DigitsToCheck The number of characters to check if they are hex.

 @return TRUE if all digits specified are hex, FALSE if not.
 */
BOOL
HexDumpDoesStringStartWithHexDigits(
    __in PYORI_STRING String,
    __in DWORD DigitsToCheck
    )
{
    DWORD Index;
    TCHAR Char;

    if (DigitsToCheck > String->LengthInChars) {
        return FALSE;
    }

    for (Index = 0; Index < DigitsToCheck; Index++) {
        Char = String->StartOfString[Index];
        if ((Char < '0' || Char > '9') &&
            (Char < 'a' || Char > 'f') &&
            (Char < 'A' || Char > 'F')) {

            return FALSE;
        }
    }

    return TRUE;
}

/**
 A structure describing the format of hex encoded text and a buffer to
 populate with binary data.
 */
typedef struct _HEXDUMP_REVERSE_CONTEXT {

    /**
     Indicates the number of characters to ignore at the beginning of the
     line. This is nonzero to ignore any offset information, which is
     meaningless here.
     */
    DWORD CharsInInputLineToIgnore;

    /**
     Indicates the number of bytes per word.  This program can process 1, 2,
     4 or 8.
     */
    DWORD BytesPerWord;

    /**
     The number of words per line.  Since the line length is fixed at
     YORI_LIB_HEXDUMP_BYTES_PER_LINE, this is really just that value divided
     by BytesPerWord.
     */
    DWORD WordsPerLine;

    /**
     The buffer to populate with data as each line is parsed.  This may point
     to StaticOutputBuffer below, or may be a heap allocation in binary mode.
     */
    PUCHAR OutputBuffer;

    /**
     A small buffer which is used initially.  This is all that is needed for
     reverse mode, but may be insufficient when processing arbitrary length
     binary lines.
     */
    UCHAR StaticOutputBuffer[YORI_LIB_HEXDUMP_BYTES_PER_LINE];

    /**
     The number of bytes of OutputBuffer that have been allocated.
     */
    DWORD BytesAllocated;

    /**
     The number of bytes of OutputBuffer that have been filled.  Note that
     on the final line the process needs to end when the output buffer is
     not completely filled.
     */
    DWORD BytesThisLine;
} HEXDUMP_REVERSE_CONTEXT, *PHEXDUMP_REVERSE_CONTEXT;

/**
 Detect the format of hex encoded text.

 @param Line Pointer to a line of hex encoded text to test.

 @param ReverseContext Pointer to the reverse hex dump context.  On output
        it is populated with the format of the data.

 @return TRUE to indicate success, FALSE to indicate failure including
         because the format is not understood.
 */
BOOL
HexDumpDetectReverseFormatFromLine(
    __in PYORI_STRING Line,
    __out PHEXDUMP_REVERSE_CONTEXT ReverseContext
    )
{
    YORI_STRING Substring;

    ReverseContext->CharsInInputLineToIgnore = 0;

    if (HexDumpDoesStringStartWithHexDigits(Line, 8)) {

        // 
        //  Check for 10 chars because there's 8 of hex, followed by a colon
        //  and space.
        //

        if (Line->LengthInChars >= 10) {
            if (Line->StartOfString[8] == ':') {
                ReverseContext->CharsInInputLineToIgnore = 8 + 2;
            } else if (Line->StartOfString[8] == '`') {

                //
                //  If the 9th char is a `, that implies it's a seperator
                //  between two 8 char sets of hex.  Verify that assumption.
                //

                YoriLibInitEmptyString(&Substring);
                Substring.StartOfString = &Line->StartOfString[9];
                Substring.LengthInChars = Line->LengthInChars - 9;
                if (HexDumpDoesStringStartWithHexDigits(&Substring, 8)) {
                    if (Substring.LengthInChars >= 10) {
                        if (Substring.StartOfString[8] == ':') {
                            ReverseContext->CharsInInputLineToIgnore = 8 * 2 + 3;
                        }
                    }
                }
            }
        }
    }

    if (Line->LengthInChars <= ReverseContext->CharsInInputLineToIgnore) {
        return FALSE;
    }

    YoriLibInitEmptyString(&Substring);
    Substring.StartOfString = &Line->StartOfString[ReverseContext->CharsInInputLineToIgnore];
    Substring.LengthInChars = Line->LengthInChars - ReverseContext->CharsInInputLineToIgnore;


    if (Substring.LengthInChars >= 2 * 4 &&
        HexDumpDoesStringStartWithHexDigits(&Substring, 2 * 4) &&
        (Substring.LengthInChars == 2 * 4 || Substring.StartOfString[2 * 4] == ' ')) {

        ReverseContext->BytesPerWord = 4;

    } else if (Substring.LengthInChars >= 2 * 2 &&
               HexDumpDoesStringStartWithHexDigits(&Substring, 2 * 2) &&
               (Substring.LengthInChars == 2 * 2 || Substring.StartOfString[2 * 2] == ' ')) {

        ReverseContext->BytesPerWord = 2;

    } else if (Substring.LengthInChars >= 2 * 1 &&
               HexDumpDoesStringStartWithHexDigits(&Substring, 2 * 1) &&
               (Substring.LengthInChars == 2 * 1 || Substring.StartOfString[2 * 1] == ' ')) {

        ReverseContext->BytesPerWord = 1;
    } else if (Substring.LengthInChars >= 2 * 4 + 1 + 2 * 4 &&
        HexDumpDoesStringStartWithHexDigits(&Substring, 2 * 4) &&
        Substring.StartOfString[2 * 4] == '`') {

        Substring.LengthInChars -= 2 * 4 + 1;
        Substring.StartOfString += 2 * 4 + 1;

        if (HexDumpDoesStringStartWithHexDigits(&Substring, 2 * 4) &&
            (Substring.LengthInChars == 2 * 4 || Substring.StartOfString[2 * 4] == ' ')) {

            ReverseContext->BytesPerWord = 8;
        }
    }

    if (ReverseContext->BytesPerWord == 0) {
        return FALSE;
    }

    ReverseContext->WordsPerLine = YORI_LIB_HEXDUMP_BYTES_PER_LINE / ReverseContext->BytesPerWord;

    return TRUE;
}

/**
 Process a word of hex encoded text into binary.

 @param String Pointer to a string containing a word of hex encoded text.

 @param ReverseContext Pointer to the reverse hex dump context.  On output
        it is populated with the binary form of data.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
HexDumpReverseParseByte(
    __in PYORI_STRING String,
    __inout PHEXDUMP_REVERSE_CONTEXT ReverseContext
    )
{
    UCHAR Value = 0;
    UCHAR ThisByte = 0;
    TCHAR SourceChar;
    DWORD BytePart;

    if (String->LengthInChars < ReverseContext->BytesPerWord * 2) {
        return FALSE;
    }

    ThisByte = 0;
    for (BytePart = 0; BytePart < 2; BytePart++) {
        ThisByte = (UCHAR)(ThisByte << 4);
        SourceChar = String->StartOfString[BytePart];
        if (SourceChar >= 'a' && SourceChar <= 'f') {
            ThisByte = (UCHAR)(ThisByte + SourceChar - 'a' + 10);
        } else if (SourceChar >= 'A' && SourceChar <= 'F') {
            ThisByte = (UCHAR)(ThisByte + SourceChar - 'A' + 10);
        } else if (SourceChar >= '0' && SourceChar <= '9') {
            ThisByte = (UCHAR)(ThisByte + SourceChar - '0');
        } else {
            return FALSE;
        }
    }
    Value = ThisByte;

    ASSERT(ReverseContext->BytesThisLine + sizeof(Value) <= ReverseContext->BytesAllocated);
    memcpy(&ReverseContext->OutputBuffer[ReverseContext->BytesThisLine], &Value, sizeof(Value));
    ReverseContext->BytesThisLine += sizeof(Value);

    return TRUE;
}

/**
 Process a word of hex encoded text into binary.

 @param String Pointer to a string containing a word of hex encoded text.

 @param ReverseContext Pointer to the reverse hex dump context.  On output
        it is populated with the binary form of data.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
HexDumpReverseParseWord(
    __in PYORI_STRING String,
    __inout PHEXDUMP_REVERSE_CONTEXT ReverseContext
    )
{
    WORD Value = 0;
    UCHAR ThisByte = 0;
    TCHAR SourceChar;
    DWORD Index;
    DWORD BytePart;

    if (String->LengthInChars < ReverseContext->BytesPerWord * 2) {
        return FALSE;
    }

    for (Index = 0; Index < ReverseContext->BytesPerWord; Index++) {
        Value = (WORD)(Value << 8);
        ThisByte = 0;
        for (BytePart = 0; BytePart < 2; BytePart++) {
            ThisByte = (UCHAR)(ThisByte << 4);
            SourceChar = String->StartOfString[Index * 2 + BytePart];
            if (SourceChar >= 'a' && SourceChar <= 'f') {
                ThisByte = (UCHAR)(ThisByte + SourceChar - 'a' + 10);
            } else if (SourceChar >= 'A' && SourceChar <= 'F') {
                ThisByte = (UCHAR)(ThisByte + SourceChar - 'A' + 10);
            } else if (SourceChar >= '0' && SourceChar <= '9') {
                ThisByte = (UCHAR)(ThisByte + SourceChar - '0');
            } else {
                return FALSE;
            }
        }
        Value = (WORD)(Value | ThisByte);
    }

    ASSERT(ReverseContext->BytesThisLine + sizeof(Value) <= ReverseContext->BytesAllocated);
    memcpy(&ReverseContext->OutputBuffer[ReverseContext->BytesThisLine], &Value, sizeof(Value));
    ReverseContext->BytesThisLine += sizeof(Value);

    return TRUE;
}

/**
 Process a word of hex encoded text into binary.

 @param String Pointer to a string containing a word of hex encoded text.

 @param ReverseContext Pointer to the reverse hex dump context.  On output
        it is populated with the binary form of data.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
HexDumpReverseParseDword(
    __in PYORI_STRING String,
    __inout PHEXDUMP_REVERSE_CONTEXT ReverseContext
    )
{
    DWORD Value = 0;
    UCHAR ThisByte = 0;
    TCHAR SourceChar;
    DWORD Index;
    DWORD BytePart;

    if (String->LengthInChars < ReverseContext->BytesPerWord * 2) {
        return FALSE;
    }

    for (Index = 0; Index < ReverseContext->BytesPerWord; Index++) {
        Value = Value << 8;
        ThisByte = 0;
        for (BytePart = 0; BytePart < 2; BytePart++) {
            ThisByte = (UCHAR)(ThisByte << 4);
            SourceChar = String->StartOfString[Index * 2 + BytePart];
            if (SourceChar >= 'a' && SourceChar <= 'f') {
                ThisByte = (UCHAR)(ThisByte + SourceChar - 'a' + 10);
            } else if (SourceChar >= 'A' && SourceChar <= 'F') {
                ThisByte = (UCHAR)(ThisByte + SourceChar - 'A' + 10);
            } else if (SourceChar >= '0' && SourceChar <= '9') {
                ThisByte = (UCHAR)(ThisByte + SourceChar - '0');
            } else {
                return FALSE;
            }
        }
        Value = Value | ThisByte;
    }

    ASSERT(ReverseContext->BytesThisLine + sizeof(Value) <= ReverseContext->BytesAllocated);
    memcpy(&ReverseContext->OutputBuffer[ReverseContext->BytesThisLine], &Value, sizeof(Value));
    ReverseContext->BytesThisLine += sizeof(Value);

    return TRUE;
}

/**
 Process a word of hex encoded text into binary.

 @param String Pointer to a string containing a word of hex encoded text.

 @param ReverseContext Pointer to the reverse hex dump context.  On output
        it is populated with the binary form of data.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
HexDumpReverseParseDwordLong(
    __in PYORI_STRING String,
    __inout PHEXDUMP_REVERSE_CONTEXT ReverseContext
    )
{
    DWORDLONG Value = 0;
    UCHAR ThisByte = 0;
    TCHAR SourceChar;
    DWORD Index;
    DWORD BytePart;
    DWORD ByteShift = 0;

    if (String->LengthInChars < ReverseContext->BytesPerWord * 2) {
        return FALSE;
    }

    for (Index = 0; Index < ReverseContext->BytesPerWord + 1; Index++) {
        if (Index == 4 && String->StartOfString[Index * 2] == '`') {
            ByteShift++;
            continue;
        }
        Value = Value << 8;
        ThisByte = 0;
        for (BytePart = 0; BytePart < 2; BytePart++) {
            ThisByte = (UCHAR)(ThisByte << 4);
            SourceChar = String->StartOfString[Index * 2 + BytePart - ByteShift];
            if (SourceChar >= 'a' && SourceChar <= 'f') {
                ThisByte = (UCHAR)(ThisByte + SourceChar - 'a' + 10);
            } else if (SourceChar >= 'A' && SourceChar <= 'F') {
                ThisByte = (UCHAR)(ThisByte + SourceChar - 'A' + 10);
            } else if (SourceChar >= '0' && SourceChar <= '9') {
                ThisByte = (UCHAR)(ThisByte + SourceChar - '0');
            } else {
                return FALSE;
            }
        }
        Value = Value | ThisByte;
    }

    ASSERT(ReverseContext->BytesThisLine + sizeof(Value) <= ReverseContext->BytesAllocated);
    memcpy(&ReverseContext->OutputBuffer[ReverseContext->BytesThisLine], &Value, sizeof(Value));
    ReverseContext->BytesThisLine += sizeof(Value);

    return TRUE;
}

/**
 Process a line of hex encoded text into binary.  The format must have been
 determined prior to this point.

 @param Line Pointer to a line of hex encoded text.

 @param ReverseContext Pointer to the reverse hex dump context.  On input
        indicates the format and it is populated with the binary form on
        output.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
HexDumpReverseParseLine(
    __in PYORI_STRING Line,
    __inout PHEXDUMP_REVERSE_CONTEXT ReverseContext
    )
{
    YORI_STRING Substring;
    DWORD StartChar;
    DWORD Index;
    BOOL StopProcessing = FALSE;

    YoriLibInitEmptyString(&Substring);
    if (Line->LengthInChars < ReverseContext->CharsInInputLineToIgnore) {
        return FALSE;
    }

    ReverseContext->BytesThisLine = 0;

    for (Index = 0; Index < ReverseContext->WordsPerLine; Index++) {
        StartChar = ReverseContext->CharsInInputLineToIgnore + Index * (ReverseContext->BytesPerWord * 2 + 1);

        //
        //  8 byte words have a seperator, so they consist of 17 raw chars
        //

        if (ReverseContext->BytesPerWord == 8 && Index > 0) {
            StartChar += Index;
        }

        if (Line->LengthInChars <= StartChar) {
            return TRUE;
        }

        Substring.StartOfString = &Line->StartOfString[StartChar];
        Substring.LengthInChars = Line->LengthInChars - StartChar;

        switch(ReverseContext->BytesPerWord) {
            case 1:
                if (!HexDumpReverseParseByte(&Substring, ReverseContext)) {
                    StopProcessing = TRUE;
                }
                break;
            case 2:
                if (!HexDumpReverseParseWord(&Substring, ReverseContext)) {
                    StopProcessing = TRUE;
                }
                break;
            case 4:
                if (!HexDumpReverseParseDword(&Substring, ReverseContext)) {
                    StopProcessing = TRUE;
                }
                break;
            case 8:
                if (!HexDumpReverseParseDwordLong(&Substring, ReverseContext)) {
                    StopProcessing = TRUE;
                }
                break;
            default:
                return FALSE;
        }

        if (StopProcessing) {
            return TRUE;
        }
    }

    return TRUE;
}

/**
 Convert a single stream of hex encoded input into binary output.  The format
 of the input is detected heuristically, but is expected to be one that can
 be output by hexdump.  That implies it may have a leading offset field or
 characters at the end of each line.  For that reason, this is not suitable
 for interpreting a flat array of hex data.

 @param hSource Handle to a source stream containing hex encoded data.

 @param HexDumpContext Pointer to hex dump context.  Currently unused for
        reverse processing.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
HexDumpReverseProcessStream(
    __in HANDLE hSource,
    __in PHEXDUMP_CONTEXT HexDumpContext
    )
{
    PVOID LineContext = NULL;
    YORI_STRING LineString;
    HANDLE OutputHandle;
    DWORD BytesWritten;
    HEXDUMP_REVERSE_CONTEXT ReverseContext;

    YoriLibInitEmptyString(&LineString);
    HexDumpContext->FilesFound++;
    HexDumpContext->FilesFoundThisArg++;

    if (!YoriLibReadLineToString(&LineString, &LineContext, hSource)) {
        return TRUE;
    }

    if (!HexDumpDetectReverseFormatFromLine(&LineString, &ReverseContext)) {
        YoriLibLineReadCloseOrCache(LineContext);
        YoriLibFreeStringContents(&LineString);
        return FALSE;
    }

    ReverseContext.OutputBuffer = ReverseContext.StaticOutputBuffer;
    ReverseContext.BytesAllocated = sizeof(ReverseContext.StaticOutputBuffer);
    OutputHandle = GetStdHandle(STD_OUTPUT_HANDLE);

    while (TRUE) {

        if (!HexDumpReverseParseLine(&LineString, &ReverseContext)) {
            break;
        }

        WriteFile(OutputHandle, ReverseContext.OutputBuffer, ReverseContext.BytesThisLine, &BytesWritten, NULL);

        if (!YoriLibReadLineToString(&LineString, &LineContext, hSource)) {
            break;
        }
    }

    YoriLibLineReadCloseOrCache(LineContext);
    YoriLibFreeStringContents(&LineString);

    return TRUE;
}

/**
 Process a line of hex encoded text into binary.  The format must have been
 determined prior to this point.

 @param Line Pointer to a line of hex encoded text.

 @param ReverseContext Pointer to the reverse hex dump context.  On input
        indicates the format and it is populated with the binary form on
        output.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
HexDumpBinaryParseLine(
    __in PYORI_STRING Line,
    __inout PHEXDUMP_REVERSE_CONTEXT ReverseContext
    )
{
    YORI_STRING Substring;
    DWORD StartChar;
    DWORD Index;
    BOOL Result;

    YoriLibInitEmptyString(&Substring);

    //
    //  Trim trailing spaces.
    //

    while (Line->LengthInChars > 0 &&
           Line->StartOfString[Line->LengthInChars - 1] == ' ') {

        Line->LengthInChars--;
    }

    ReverseContext->BytesThisLine = 0;

    Index = 0;
    Result = TRUE;

    while(TRUE) {

        StartChar = Index * (ReverseContext->BytesPerWord * 2 + 1);

        //
        //  8 byte words have a seperator, so they consist of 17 raw chars
        //

        if (ReverseContext->BytesPerWord == 8 && Index > 0) {
            StartChar += Index;
        }

        //
        //  If the first char to parse is at the end of the line, this line
        //  is finished successfully.  Note that it might be beyond the end
        //  of the line due to a space, but that still constitutes success.
        //

        if (StartChar >= Line->LengthInChars) {
            break;
        }

        //
        //  If the line has more characters but not enough for a word, that
        //  indicates parse failure.  We don't know what these characters
        //  are for.
        //

        if (StartChar + ReverseContext->BytesPerWord > Line->LengthInChars) {
            break;
        }

        //
        //  If there is more data to output than the buffer can hold, grow
        //  the buffer.  Since the buffer is reused across lines, grow it
        //  substantially to avoid doing this too often.
        //

        if (ReverseContext->BytesThisLine + ReverseContext->BytesPerWord > ReverseContext->BytesAllocated) {
            PUCHAR NewBuffer;
            DWORD NewLength;

            NewLength = ReverseContext->BytesAllocated * 4;
            if (NewLength < 0x800) {
                NewLength = 0x800;
            }

            NewBuffer = YoriLibMalloc(NewLength);
            if (NewBuffer == NULL) {
                Result = FALSE;
                break;
            }

            memcpy(NewBuffer, ReverseContext->OutputBuffer, ReverseContext->BytesThisLine);
            if (ReverseContext->OutputBuffer != ReverseContext->StaticOutputBuffer) {
                YoriLibFree(ReverseContext->OutputBuffer);
            }

            ReverseContext->OutputBuffer = NewBuffer;
            ReverseContext->BytesAllocated = NewLength;
        }

        Substring.StartOfString = &Line->StartOfString[StartChar];
        Substring.LengthInChars = Line->LengthInChars - StartChar;

        switch(ReverseContext->BytesPerWord) {
            case 1:
                if (!HexDumpReverseParseByte(&Substring, ReverseContext)) {
                    Result = FALSE;
                }
                break;
            case 2:
                if (!HexDumpReverseParseWord(&Substring, ReverseContext)) {
                    Result = FALSE;
                }
                break;
            case 4:
                if (!HexDumpReverseParseDword(&Substring, ReverseContext)) {
                    Result = FALSE;
                }
                break;
            case 8:
                if (!HexDumpReverseParseDwordLong(&Substring, ReverseContext)) {
                    Result = FALSE;
                }
                break;
            default:
                Result = FALSE;
                break;
        }

        if (Result == FALSE) {
            break;
        }

        Index++;
    }

    return Result;
}

/**
 Convert a single stream of hex encoded input into binary output.  The format
 of the input can contain any supported word length.

 @param hSource Handle to a source stream containing hex encoded data.

 @param HexDumpContext Pointer to hex dump context.  Currently unused for
        reverse processing.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
HexDumpBinaryProcessStream(
    __in HANDLE hSource,
    __in PHEXDUMP_CONTEXT HexDumpContext
    )
{
    PVOID LineContext = NULL;
    YORI_STRING LineString;
    HANDLE OutputHandle;
    DWORD BytesWritten;
    DWORDLONG LineNumber;
    HEXDUMP_REVERSE_CONTEXT ReverseContext;

    YoriLibInitEmptyString(&LineString);
    HexDumpContext->FilesFound++;
    HexDumpContext->FilesFoundThisArg++;

    LineNumber = 0;

    if (!YoriLibReadLineToString(&LineString, &LineContext, hSource)) {
        return TRUE;
    }

    LineNumber++;

    if (!HexDumpDetectReverseFormatFromLine(&LineString, &ReverseContext)) {
        YoriLibLineReadCloseOrCache(LineContext);
        YoriLibFreeStringContents(&LineString);
        return FALSE;
    }

    if (ReverseContext.CharsInInputLineToIgnore != 0) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("hexdump: not a stream of hex digits, line %lli\n"), LineNumber);
        YoriLibLineReadCloseOrCache(LineContext);
        YoriLibFreeStringContents(&LineString);
        return FALSE;
    }
    ReverseContext.OutputBuffer = ReverseContext.StaticOutputBuffer;
    ReverseContext.BytesAllocated = sizeof(ReverseContext.StaticOutputBuffer);

    OutputHandle = GetStdHandle(STD_OUTPUT_HANDLE);

    while (TRUE) {

        if (!HexDumpBinaryParseLine(&LineString, &ReverseContext)) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("hexdump: not a stream of hex digits, line %lli\n"), LineNumber);
            break;
        }

        if (ReverseContext.BytesThisLine > 0) {
            WriteFile(OutputHandle, ReverseContext.OutputBuffer, ReverseContext.BytesThisLine, &BytesWritten, NULL);
        }

        if (!YoriLibReadLineToString(&LineString, &LineContext, hSource)) {
            break;
        }

        LineNumber++;
    }

    if (ReverseContext.OutputBuffer != ReverseContext.StaticOutputBuffer) {
        YoriLibFree(ReverseContext.OutputBuffer);
        ReverseContext.OutputBuffer = NULL;
    }

    YoriLibLineReadCloseOrCache(LineContext);
    YoriLibFreeStringContents(&LineString);

    return TRUE;
}


/**
 Process a single opened stream, enumerating through all lines and displaying
 the set requested by the user.

 @param hSource The opened source stream.

 @param HexDumpContext Pointer to context information specifying which lines to
        display.
 
 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
HexDumpProcessStream(
    __in HANDLE hSource,
    __in PHEXDUMP_CONTEXT HexDumpContext
    )
{
    PUCHAR Buffer;
    DWORD BufferSize;
    DWORD BufferReadOffset;
    DWORD BufferDisplayOffset;
    DWORD BytesReturned;
    DWORD LengthToDisplay;
    DWORD DisplayFlags;
    DWORD FileType;
    LARGE_INTEGER StreamOffset;
    BOOLEAN LimitDisplayToEvenLine;

    HexDumpContext->FilesFound++;
    HexDumpContext->FilesFoundThisArg++;

    BufferSize = 64 * 1024;
    Buffer = YoriLibMalloc(BufferSize);
    if (Buffer == NULL) {
        return FALSE;
    }
    DisplayFlags = 0;
    if (!HexDumpContext->HideOffset) {
        DisplayFlags |= YORI_LIB_HEX_FLAG_DISPLAY_LARGE_OFFSET;
    }
    if (!HexDumpContext->HideCharacters) {
        if (HexDumpContext->WideCharacters) {
            DisplayFlags |= YORI_LIB_HEX_FLAG_DISPLAY_WCHARS;
        } else {
            DisplayFlags |= YORI_LIB_HEX_FLAG_DISPLAY_CHARS;
        }
    }
    if (HexDumpContext->CStyleInclude) {
        DisplayFlags |= YORI_LIB_HEX_FLAG_C_STYLE;
    }

    //
    //  If it's a file, start at the offset requested by the user.  If it's
    //  not a file (it's a pipe), the only way to move forward is by
    //  reading.
    //

    FileType = GetFileType(hSource);

    StreamOffset.QuadPart = 0;
    if (FileType != FILE_TYPE_PIPE) {
        StreamOffset.QuadPart = HexDumpContext->OffsetToDisplay;

        if (!SetFilePointer(hSource, StreamOffset.LowPart, &StreamOffset.HighPart, FILE_BEGIN)) {
            StreamOffset.QuadPart = 0;
        }
    }

    BufferReadOffset = 0;

    while (TRUE) {

        //
        //  Read a block of data.  On a pipe, this will block.
        //

        BytesReturned = 0;
        ASSERT(BufferReadOffset < BufferSize);
        if (!ReadFile(hSource, Buffer + BufferReadOffset, BufferSize - BufferReadOffset, &BytesReturned, NULL)) {
            BytesReturned = 0;
        }

        //
        //  Add back whatever data was carried over from previous reads to
        //  the amount from this read.  If we don't have data from either
        //  source despite blocking, the operation is complete.
        //

        BytesReturned = BytesReturned + BufferReadOffset;
        if (BytesReturned == 0) {
            break;
        }

        //
        //  If we haven't reached the starting point to display, loop back
        //  and read more.
        //

        if (StreamOffset.QuadPart + BytesReturned <= HexDumpContext->OffsetToDisplay) {
            StreamOffset.QuadPart += BytesReturned;
            BufferReadOffset = 0;
            continue;
        }

        LengthToDisplay = BytesReturned;

        //
        //  If the starting point to display is partway through the buffer,
        //  find the offset within the buffer to start displaying and cap
        //  the number of characters to display.
        //

        BufferDisplayOffset = 0;
        if (StreamOffset.QuadPart < HexDumpContext->OffsetToDisplay) {
            BufferDisplayOffset = (DWORD)(HexDumpContext->OffsetToDisplay - StreamOffset.QuadPart);
            LengthToDisplay -= BufferDisplayOffset;
        }

        ASSERT(BufferDisplayOffset + LengthToDisplay == BytesReturned);

        //
        //  If the number of bytes that the user requested to display is
        //  longer than the amount we have, cap the amount to display to
        //  what the user requested.
        //

        LimitDisplayToEvenLine = TRUE;
        if (HexDumpContext->LengthToDisplay != 0) {
            if (StreamOffset.QuadPart + BufferDisplayOffset + LengthToDisplay >= HexDumpContext->OffsetToDisplay + HexDumpContext->LengthToDisplay) {
                LengthToDisplay = (DWORD)(HexDumpContext->OffsetToDisplay + HexDumpContext->LengthToDisplay - StreamOffset.QuadPart - BufferDisplayOffset);
                LimitDisplayToEvenLine = FALSE;
            }
        }

        //
        //  If ReadFile didn't return any new data, but we still have data
        //  leftover, then display what we have.
        //

        if (BytesReturned == BufferReadOffset) {
            LimitDisplayToEvenLine = FALSE;
        }

        //
        //  Try to display a multiple of HEXDUMP_BYTES_PER_LINE.  If there's
        //  more data, count how many bytes are leftover.  This number will
        //  be copied to the beginning of the buffer after display.
        //

        if (LimitDisplayToEvenLine) {
            BufferReadOffset = LengthToDisplay % YORI_LIB_HEXDUMP_BYTES_PER_LINE;
            LengthToDisplay = LengthToDisplay - BufferReadOffset;
        }

        //
        //  Display the buffer at the display offset for the length to
        //  display.
        //

        if (LengthToDisplay > 0) {
            if (!YoriLibHexDump((LPCSTR)&Buffer[BufferDisplayOffset], StreamOffset.QuadPart + BufferDisplayOffset, LengthToDisplay, HexDumpContext->BytesPerGroup, DisplayFlags)) {
                break;
            }
        }

        //
        //  If there is leftover data, copy it to the beginning of the buffer.
        //

        if (BufferReadOffset > 0 && BufferDisplayOffset + LengthToDisplay != 0) {
            memmove(Buffer, &Buffer[BufferDisplayOffset + LengthToDisplay], BufferReadOffset);
        }

        //
        //  Move the stream forward to the end of the buffer that was
        //  displayed.  There may be more data in the buffer, which will
        //  be handled on the next loop iteration.
        //

        StreamOffset.QuadPart += BufferDisplayOffset + LengthToDisplay;

        if (!LimitDisplayToEvenLine) {
            break;
        }

        if (HexDumpContext->LengthToDisplay != 0 &&
            StreamOffset.QuadPart >= HexDumpContext->OffsetToDisplay + HexDumpContext->LengthToDisplay) {

            break;
        }
    }

    YoriLibFree(Buffer);

    return TRUE;
}

/**
 Prototype for a function which can perform the user's requested action on a
 successfully opened stream.
 */
typedef BOOL HEXDUMP_PROCESS_STREAM_FN(HANDLE hSource, PHEXDUMP_CONTEXT HexDumpContext);

/**
 Pointer to a function which can perform the user's requested action on a
 successfully opened stream.
 */
typedef HEXDUMP_PROCESS_STREAM_FN *PHEXDUMP_PROCESS_STREAM_FN;

/**
 A callback that is invoked when a file is found that matches a search criteria
 specified in the set of strings to enumerate.  This function takes a function
 pointer to invoke in order to process any successfully opened file.

 @param FilePath Pointer to the file path that was found.

 @param FileInfo Information about the file.  Can be NULL if the file being
        opened was not found by enumeration.

 @param HexDumpContext Pointer to the hexdump context structure indicating the
        action to perform and populated with the file and line count found.

 @param Fn Pointer to a function to invoke if the file can be successfully
        opened.  This function is expected to perform the requested processing
        on the file.

 @return TRUE to continute enumerating, FALSE to abort.
 */
BOOL
HexDumpCommonFileFoundCallback(
    __in PYORI_STRING FilePath,
    __in_opt PWIN32_FIND_DATA FileInfo,
    __in PHEXDUMP_CONTEXT HexDumpContext,
    __in PHEXDUMP_PROCESS_STREAM_FN Fn
    )
{
    HANDLE FileHandle;

    ASSERT(YoriLibIsStringNullTerminated(FilePath));

    if (FileInfo == NULL ||
        (FileInfo->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) {

        FileHandle = CreateFile(FilePath->StartOfString,
                                GENERIC_READ,
                                FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                                NULL,
                                OPEN_EXISTING,
                                FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS,
                                NULL);

        if (FileHandle == NULL || FileHandle == INVALID_HANDLE_VALUE) {
            if (HexDumpContext->SavedErrorThisArg == ERROR_SUCCESS) {
                DWORD LastError = GetLastError();
                LPTSTR ErrText = YoriLibGetWinErrorText(LastError);
                YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("hexdump: open of %y failed: %s"), FilePath, ErrText);
                YoriLibFreeWinErrorText(ErrText);
            }
            return TRUE;
        }

        HexDumpContext->SavedErrorThisArg = ERROR_SUCCESS;
        Fn(FileHandle, HexDumpContext);

        CloseHandle(FileHandle);
    }

    return TRUE;
}

/**
 A callback that is invoked when a file is found that matches a search criteria
 specified in the set of strings to enumerate.

 @param FilePath Pointer to the file path that was found.

 @param FileInfo Information about the file.  Can be NULL if the file being
        opened was not found by enumeration.

 @param Depth Specifies recursion depth.  Ignored in this application.

 @param Context Pointer to the hexdump context structure indicating the
        action to perform and populated with the file and line count found.

 @return TRUE to continute enumerating, FALSE to abort.
 */
BOOL
HexDumpFileFoundCallback(
    __in PYORI_STRING FilePath,
    __in_opt PWIN32_FIND_DATA FileInfo,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    PHEXDUMP_CONTEXT HexDumpContext = (PHEXDUMP_CONTEXT)Context;
    UNREFERENCED_PARAMETER(Depth);
    return HexDumpCommonFileFoundCallback(FilePath, FileInfo, HexDumpContext, HexDumpProcessStream);
}

/**
 A callback that is invoked when a file is found that matches a search criteria
 specified in the set of strings to enumerate.

 @param FilePath Pointer to the file path that was found.

 @param FileInfo Information about the file.  Can be NULL if the file being
        opened was not found by enumeration.

 @param Depth Specifies recursion depth.  Ignored in this application.

 @param Context Pointer to the hexdump context structure indicating the
        action to perform and populated with the file and line count found.

 @return TRUE to continute enumerating, FALSE to abort.
 */
BOOL
HexDumpReverseFileFoundCallback(
    __in PYORI_STRING FilePath,
    __in_opt PWIN32_FIND_DATA FileInfo,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    PHEXDUMP_CONTEXT HexDumpContext = (PHEXDUMP_CONTEXT)Context;
    UNREFERENCED_PARAMETER(Depth);
    return HexDumpCommonFileFoundCallback(FilePath, FileInfo, HexDumpContext, HexDumpReverseProcessStream);
}

/**
 A callback that is invoked when a file is found that matches a search criteria
 specified in the set of strings to enumerate.

 @param FilePath Pointer to the file path that was found.

 @param FileInfo Information about the file.  Can be NULL if the file being
        opened was not found by enumeration.

 @param Depth Specifies recursion depth.  Ignored in this application.

 @param Context Pointer to the hexdump context structure indicating the
        action to perform and populated with the file and line count found.

 @return TRUE to continute enumerating, FALSE to abort.
 */
BOOL
HexDumpBinaryFileFoundCallback(
    __in PYORI_STRING FilePath,
    __in_opt PWIN32_FIND_DATA FileInfo,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    PHEXDUMP_CONTEXT HexDumpContext = (PHEXDUMP_CONTEXT)Context;
    UNREFERENCED_PARAMETER(Depth);
    return HexDumpCommonFileFoundCallback(FilePath, FileInfo, HexDumpContext, HexDumpBinaryProcessStream);
}

/**
 A callback that is invoked when a directory cannot be successfully enumerated.

 @param FilePath Pointer to the file path that could not be enumerated.

 @param ErrorCode The Win32 error code describing the failure.

 @param Depth Recursion depth, ignored in this application.

 @param Context Pointer to the context block indicating whether the
        enumeration was recursive.  Recursive enumerates do not complain
        if a matching file is not in every single directory, because
        common usage expects files to be in a subset of directories only.

 @return TRUE to continute enumerating, FALSE to abort.
 */
BOOL
HexDumpFileEnumerateErrorCallback(
    __in PYORI_STRING FilePath,
    __in DWORD ErrorCode,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    YORI_STRING UnescapedFilePath;
    BOOL Result = FALSE;
    PHEXDUMP_CONTEXT HexDumpContext = (PHEXDUMP_CONTEXT)Context;

    UNREFERENCED_PARAMETER(Depth);

    YoriLibInitEmptyString(&UnescapedFilePath);
    if (!YoriLibUnescapePath(FilePath, &UnescapedFilePath)) {
        UnescapedFilePath.StartOfString = FilePath->StartOfString;
        UnescapedFilePath.LengthInChars = FilePath->LengthInChars;
    }

    if (ErrorCode == ERROR_FILE_NOT_FOUND || ErrorCode == ERROR_PATH_NOT_FOUND) {
        if (!HexDumpContext->Recursive) {
            HexDumpContext->SavedErrorThisArg = ErrorCode;
        }
        Result = TRUE;
    } else {
        LPTSTR ErrText = YoriLibGetWinErrorText(ErrorCode);
        YORI_STRING DirName;
        LPTSTR FilePart;
        YoriLibInitEmptyString(&DirName);
        DirName.StartOfString = UnescapedFilePath.StartOfString;
        FilePart = YoriLibFindRightMostCharacter(&UnescapedFilePath, '\\');
        if (FilePart != NULL) {
            DirName.LengthInChars = (DWORD)(FilePart - DirName.StartOfString);
        } else {
            DirName.LengthInChars = UnescapedFilePath.LengthInChars;
        }
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Enumerate of %y failed: %s"), &DirName, ErrText);
        YoriLibFreeWinErrorText(ErrText);
    }
    YoriLibFreeStringContents(&UnescapedFilePath);
    return Result;
}


/**
 Context corresponding to a single source when displaying differences
 between two sources.
 */
typedef struct _HEXDUMP_ONE_OBJECT {

    /**
     A full path expanded for this source.
     */
    YORI_STRING FullFileName;

    /**
     A handle to the source of this data.
     */
    HANDLE FileHandle;

    /**
     A buffer to hold data read from this source.
     */
    PUCHAR Buffer;

    /**
     The number of bytes read from this source.
     */
    DWORD BytesReturned;

    /**
     Set to TRUE if a read operation from this source has failed.
     */
    BOOL ReadFailed;

    /**
     The number of bytes to display for a given line from this
     buffer.  This is recalculated for each line based on the source's
     buffer length.
     */
    DWORD DisplayLength;
} HEXDUMP_ONE_OBJECT, *PHEXDUMP_ONE_OBJECT;

/**
 Display the differences between two files in hex form.

 @param FileA The name of the first file, without any full path expansion.

 @param FileB The name of the second file, without any full path expansion.

 @param HexDumpContext Pointer to the context indicating display parameters.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
HexDumpDisplayDiff(
    __in PYORI_STRING FileA,
    __in PYORI_STRING FileB,
    __in PHEXDUMP_CONTEXT HexDumpContext
    )
{
    HEXDUMP_ONE_OBJECT Objects[2];
    DWORD BufferSize;
    DWORD BufferOffset;
    DWORD LengthToDisplay;
    DWORD LengthThisLine;
    DWORD DisplayFlags;
    LARGE_INTEGER StreamOffset;
    DWORD Count;
    BOOL Result = FALSE;
    BOOL LineDifference;

    BufferSize = 64 * 1024;
    DisplayFlags = 0;
    if (!HexDumpContext->HideOffset) {
        DisplayFlags |= YORI_LIB_HEX_FLAG_DISPLAY_LARGE_OFFSET;
    }
    if (!HexDumpContext->HideCharacters) {
        if (HexDumpContext->WideCharacters) {
            DisplayFlags |= YORI_LIB_HEX_FLAG_DISPLAY_WCHARS;
        } else {
            DisplayFlags |= YORI_LIB_HEX_FLAG_DISPLAY_CHARS;
        }
    }
    StreamOffset.QuadPart = HexDumpContext->OffsetToDisplay;

    ZeroMemory(Objects, sizeof(Objects));

    for (Count = 0; Count < sizeof(Objects)/sizeof(Objects[0]); Count++) {

        //
        //  Resolve the file to a full path
        //

        YoriLibInitEmptyString(&Objects[Count].FullFileName);
        if (Count == 0) {
            if (!YoriLibUserStringToSingleFilePath(FileA, TRUE, &Objects[Count].FullFileName)) {
                YoriLibInitEmptyString(&Objects[Count].FullFileName);
                goto Exit;
            }
        } else {
            if (!YoriLibUserStringToSingleFilePath(FileB, TRUE, &Objects[Count].FullFileName)) {
                YoriLibInitEmptyString(&Objects[Count].FullFileName);
                goto Exit;
            }
        }

        //
        //  Open each file
        //

        Objects[Count].FileHandle = CreateFile(Objects[Count].FullFileName.StartOfString,
                                               GENERIC_READ,
                                               FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                                               NULL,
                                               OPEN_EXISTING,
                                               FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS,
                                               NULL);

        if (Objects[Count].FileHandle == NULL || Objects[Count].FileHandle == INVALID_HANDLE_VALUE) {
            DWORD LastError = GetLastError();
            LPTSTR ErrText = YoriLibGetWinErrorText(LastError);
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("hexdump: open of %y failed: %s"), &Objects[Count].FullFileName, ErrText);
            YoriLibFreeWinErrorText(ErrText);
            goto Exit;
        }

        //
        //  Allocate a read buffer for the file
        //

        Objects[Count].Buffer = YoriLibMalloc(BufferSize);
        if (Objects[Count].Buffer == NULL) {
            goto Exit;
        }

        //
        //  Seek to the requested offset in the file.  Note that in the diff
        //  case we have files, so seeking is valid.
        //

        SetFilePointer(Objects[Count].FileHandle, StreamOffset.LowPart, &StreamOffset.HighPart, FILE_BEGIN);
        if (GetLastError() != NO_ERROR) {
            DWORD LastError = GetLastError();
            LPTSTR ErrText = YoriLibGetWinErrorText(LastError);
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("hexdump: seek of %y failed: %s"), &Objects[Count].FullFileName, ErrText);
            YoriLibFreeWinErrorText(ErrText);
            goto Exit;
        }
    }

    while (TRUE) {

        //
        //  Read from each file
        //

        for (Count = 0; Count < sizeof(Objects)/sizeof(Objects[0]); Count++) {
            Objects[Count].ReadFailed = FALSE;
            Objects[Count].BytesReturned = 0;
            if (!ReadFile(Objects[Count].FileHandle, Objects[Count].Buffer, BufferSize, &Objects[Count].BytesReturned, NULL)) {
                Objects[Count].ReadFailed = TRUE;
                Objects[Count].BytesReturned = 0;
            } else if (Objects[Count].BytesReturned == 0) {
                Objects[Count].ReadFailed = TRUE;
            }
        }

        //
        //  If we've finished both sources, we are done.
        //

        if (Objects[0].ReadFailed && Objects[1].ReadFailed) {
            break;
        }

        //
        //  Display the maximum of what was read between the two
        //

        if (Objects[0].BytesReturned > Objects[1].BytesReturned) {
            LengthToDisplay = Objects[0].BytesReturned;
        } else {
            LengthToDisplay = Objects[1].BytesReturned;
        }

        //
        //  Truncate the display to the range the user requested
        //

        if (HexDumpContext->LengthToDisplay != 0) {
            if (StreamOffset.QuadPart + LengthToDisplay >= HexDumpContext->OffsetToDisplay + HexDumpContext->LengthToDisplay) {
                LengthToDisplay = (DWORD)(HexDumpContext->OffsetToDisplay + HexDumpContext->LengthToDisplay - StreamOffset.QuadPart);
                if (LengthToDisplay == 0) {
                    break;
                }
            }
        }

        BufferOffset = 0;

        while(BufferOffset < LengthToDisplay) {

            //
            //  Check each line to see if it's different
            //

            LineDifference = FALSE;
            if (LengthToDisplay - BufferOffset >= 16) {
                LengthThisLine = 16;
            } else {
                LengthThisLine = LengthToDisplay - BufferOffset;
            }
            for (Count = 0; Count < sizeof(Objects)/sizeof(Objects[0]); Count++) {
                Objects[Count].DisplayLength = LengthThisLine;
                if (BufferOffset + LengthThisLine > Objects[Count].BytesReturned) {
                    LineDifference = TRUE;
                    Objects[Count].DisplayLength = 0;
                    if (Objects[Count].BytesReturned > BufferOffset) {
                        Objects[Count].DisplayLength = Objects[0].BytesReturned - BufferOffset;
                    }
                }
            }

            if (!LineDifference &&
                memcmp(&Objects[0].Buffer[BufferOffset], &Objects[1].Buffer[BufferOffset], LengthThisLine) != 0) {
                LineDifference = TRUE;
            }

            //
            //  If it's different, display it
            //

            if (LineDifference) {
                if (!YoriLibHexDiff(StreamOffset.QuadPart + BufferOffset,
                                    (LPCSTR)&Objects[0].Buffer[BufferOffset],
                                    Objects[0].DisplayLength,
                                    (LPCSTR)&Objects[1].Buffer[BufferOffset],
                                    Objects[1].DisplayLength,
                                    HexDumpContext->BytesPerGroup,
                                    DisplayFlags)) {
                    break;
                }
            }

            //
            //  Move to the next line
            //

            BufferOffset += LengthThisLine;
        }

        StreamOffset.QuadPart += LengthToDisplay;
    }

Exit:

    //
    //  Clean up state from each source
    //

    for (Count = 0; Count < sizeof(Objects)/sizeof(Objects[0]); Count++) {
        if (Objects[Count].FileHandle != NULL && Objects[Count].FileHandle != INVALID_HANDLE_VALUE) {
#if defined(_MSC_VER) && (_MSC_VER >= 1700)
#pragma warning(suppress: 6001) // Analyze doesn't trust ZeroMemory, or
                                // doesn't understand the array math
#endif
            CloseHandle(Objects[Count].FileHandle);
        }
        if (Objects[Count].Buffer != NULL) {
            YoriLibFree(Objects[Count].Buffer);
        }
        YoriLibFreeStringContents(&Objects[Count].FullFileName);
    }

    return Result;
}

#ifdef YORI_BUILTIN
/**
 The main entrypoint for the hexdump builtin command.
 */
#define ENTRYPOINT YoriCmd_HEXDUMP
#else
/**
 The main entrypoint for the hexdump standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the hexdump cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process, zero indicating success or nonzero on
         failure.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOLEAN ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    DWORD MatchFlags;
    DWORD CharsConsumed;
    BOOLEAN BasicEnumeration = FALSE;
    BOOLEAN DiffMode = FALSE;
    BOOLEAN BinaryEncode = FALSE;
    BOOLEAN Reverse = FALSE;
    HEXDUMP_CONTEXT HexDumpContext;
    YORI_STRING Arg;

    ZeroMemory(&HexDumpContext, sizeof(HexDumpContext));
    HexDumpContext.BytesPerGroup = 4;

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                HexDumpHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2017-2021"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("b")) == 0) {
                BasicEnumeration = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("bin")) == 0) {
                BinaryEncode = TRUE;
                Reverse = FALSE;
                HexDumpContext.CStyleInclude = FALSE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("d")) == 0) {
                DiffMode = TRUE;
                HexDumpContext.CStyleInclude = FALSE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("g1")) == 0) {
                HexDumpContext.BytesPerGroup = 1;
                HexDumpContext.CStyleInclude = FALSE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("g2")) == 0) {
                HexDumpContext.BytesPerGroup = 2;
                HexDumpContext.CStyleInclude = FALSE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("g4")) == 0) {
                HexDumpContext.BytesPerGroup = 4;
                HexDumpContext.CStyleInclude = FALSE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("g8")) == 0) {
                HexDumpContext.BytesPerGroup = 8;
                HexDumpContext.CStyleInclude = FALSE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("hc")) == 0) {
                HexDumpContext.HideCharacters = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("ho")) == 0) {
                HexDumpContext.HideOffset = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("i")) == 0) {
                DiffMode = FALSE;
                HexDumpContext.CStyleInclude = TRUE;
                HexDumpContext.HideOffset = TRUE;
                HexDumpContext.HideCharacters = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("l")) == 0) {
                if (ArgC > i + 1) {
                    YoriLibStringToNumber(&ArgV[i + 1], TRUE, &HexDumpContext.LengthToDisplay, &CharsConsumed);
                    i++;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("o")) == 0) {
                if (ArgC > i + 1) {
                    YoriLibStringToNumber(&ArgV[i + 1], TRUE, &HexDumpContext.OffsetToDisplay, &CharsConsumed);
                    i++;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("r")) == 0) {
                Reverse = TRUE;
                BinaryEncode = FALSE;
                HexDumpContext.CStyleInclude = FALSE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("s")) == 0) {
                HexDumpContext.Recursive = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("w")) == 0) {
                HexDumpContext.WideCharacters = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                StartArg = i + 1;
                ArgumentUnderstood = TRUE;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

#if YORI_BUILTIN
    YoriLibCancelEnable();
#endif

    //
    //  Attempt to enable backup privilege so an administrator can access more
    //  objects successfully.
    //

    YoriLibEnableBackupPrivilege();

    if (DiffMode) {
        if (StartArg == 0 || StartArg + 2 > ArgC) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("hexdump: insufficient arguments\n"));
            return EXIT_FAILURE;
        }

        if (!HexDumpDisplayDiff(&ArgV[StartArg], &ArgV[StartArg + 1], &HexDumpContext)) {
            return EXIT_FAILURE;
        }
        return EXIT_SUCCESS;
    }

    //
    //  If no file name is specified, use stdin; otherwise open
    //  the file and use that
    //

    if (StartArg == 0 || StartArg == ArgC) {
        if (YoriLibIsStdInConsole()) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("No file or pipe for input\n"));
            return EXIT_FAILURE;
        }

        if (BinaryEncode) {
            HexDumpBinaryProcessStream(GetStdHandle(STD_INPUT_HANDLE), &HexDumpContext);
        } else if (Reverse) {
            HexDumpReverseProcessStream(GetStdHandle(STD_INPUT_HANDLE), &HexDumpContext);
        } else {
            HexDumpProcessStream(GetStdHandle(STD_INPUT_HANDLE), &HexDumpContext);
        }
    } else {
        MatchFlags = YORILIB_FILEENUM_RETURN_FILES | YORILIB_FILEENUM_DIRECTORY_CONTENTS;
        if (HexDumpContext.Recursive) {
            MatchFlags |= YORILIB_FILEENUM_RECURSE_BEFORE_RETURN | YORILIB_FILEENUM_RECURSE_PRESERVE_WILD;
        }
        if (BasicEnumeration) {
            MatchFlags |= YORILIB_FILEENUM_BASIC_EXPANSION;
        }

        for (i = StartArg; i < ArgC; i++) {

            HexDumpContext.FilesFoundThisArg = 0;
            HexDumpContext.SavedErrorThisArg = ERROR_SUCCESS;

            if (BinaryEncode) {
                YoriLibForEachStream(&ArgV[i],
                                     MatchFlags,
                                     0,
                                     HexDumpBinaryFileFoundCallback,
                                     HexDumpFileEnumerateErrorCallback,
                                     &HexDumpContext);
            } else if (Reverse) {
                YoriLibForEachStream(&ArgV[i],
                                     MatchFlags,
                                     0,
                                     HexDumpReverseFileFoundCallback,
                                     HexDumpFileEnumerateErrorCallback,
                                     &HexDumpContext);
            } else {
                YoriLibForEachStream(&ArgV[i],
                                     MatchFlags,
                                     0,
                                     HexDumpFileFoundCallback,
                                     HexDumpFileEnumerateErrorCallback,
                                     &HexDumpContext);
            }

            if (HexDumpContext.FilesFoundThisArg == 0) {
                YORI_STRING FullPath;
                YoriLibInitEmptyString(&FullPath);
                if (YoriLibUserStringToSingleFilePath(&ArgV[i], TRUE, &FullPath)) {
                    if (BinaryEncode) {
                        HexDumpBinaryFileFoundCallback(&FullPath, NULL, 0, &HexDumpContext);
                    } else if (Reverse) {
                        HexDumpReverseFileFoundCallback(&FullPath, NULL, 0, &HexDumpContext);
                    } else {
                        HexDumpFileFoundCallback(&FullPath, NULL, 0, &HexDumpContext);
                    }
                    YoriLibFreeStringContents(&FullPath);
                }
                if (HexDumpContext.SavedErrorThisArg != ERROR_SUCCESS) {
                    YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("File or directory not found: %y\n"), &ArgV[i]);
                }
            }
        }
    }

#if !YORI_BUILTIN
    YoriLibLineReadCleanupCache();
#endif

    if (HexDumpContext.FilesFound == 0) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("hexdump: no matching files found\n"));
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>lib/malloc.c<|end_filename|>
/**
 * @file lib/malloc.c
 *
 * Yori memory allocation wrappers
 *
 * Copyright (c) 2017-2019 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "yoripch.h"
#include "yorilib.h"

#if YORI_SPECIAL_HEAP

/**
 The number of stack frames to capture when the OS supports it.
 */
#define YORI_SPECIAL_HEAP_STACK_FRAMES (10)

/**
 A structure embedded at the top of special heap allocations.
 */
typedef struct _YORI_SPECIAL_HEAP_HEADER {

    /**
     Offset in bytes from the top of the page to the user's allocation.
     */
    DWORD OffsetToData;

    /**
     Number of pages in the allocation.  Special heap allocations are
     always a multiple of pages.
     */
    DWORD PagesInAllocation;

    /**
     List of all active memory allocations.
     */
    YORI_LIST_ENTRY ListEntry;

    /**
     The function that allocated the memory.
     */
    LPCSTR Function;

    /**
     The source file that allocated the memory.
     */
    LPCSTR File;

    /**
     The line number that allocated the memory.
     */
    DWORD Line;

    /**
     An extra 32 bits that does nothing to ensure 64 bit alignment of this
     structure so that 64 bit builds will capture pointer values on 64 bit
     boundaries.
     */
    DWORD ReservedForAlignment;

    /**
     This structure may be followed by the stack that allocated this
     allocation.
     */

} YORI_SPECIAL_HEAP_HEADER, *PYORI_SPECIAL_HEAP_HEADER;

/**
 The number of bytes in a page on this architecture.
 */
#define PAGE_SIZE (0x1000)

typedef struct _YORI_SPECIAL_HEAP_GLOBAL {

    /**
     The number of allocations that have previously been allocated by the caller.
     */
    DWORD NumberAllocated;

    /**
     The number of allocations that have previously been freed by the caller.
     */
    DWORD NumberFreed;

    /**
     The number of bytes currently allocated, from the user's perspective.
     */
    DWORD BytesCurrentlyAllocated;

    /**
     A list of all active allocations in the system.
     */
    YORI_LIST_ENTRY ActiveAllocationsList;

    /**
     An array of recently freed allocations.  Each of these is accessed in a
     circular fashion, so that the least recently freed object is replaced by
     a new incoming free.  While on this array allocations are not readable
     or writable so that use after free bugs become visibly apparent.
     */
    PYORI_SPECIAL_HEAP_HEADER RecentlyFreed[8];

    /**
     A handle to a mutex to synchronize the debug heap.
     */
    HANDLE Mutex;

} YORI_SPECIAL_HEAP_GLOBAL, PYORI_SPECIAL_HEAP_GLOBAL;

YORI_SPECIAL_HEAP_GLOBAL YoriLibSpecialHeap;

#endif

#if !YORI_SPECIAL_HEAP
/**
 Allocate memory.  This should be freed with @ref YoriLibFree when it is no
 longer needed.

 @param Bytes The number of bytes to allocate.

 @return A pointer to the newly allocated memory, or NULL on failure.
 */
PVOID
YoriLibMalloc(
    __in DWORD Bytes
    )
{
    PVOID Alloc;
    Alloc = HeapAlloc(GetProcessHeap(), 0, Bytes);
    return Alloc;
}
#else

#if defined(_MSC_VER) && (_MSC_VER >= 1500)
#pragma warning(disable: 6250) // Calling VirtualFree without MEM_RELEASE,
                               // this module uses two calls
#endif
/**
 Allocate memory.  This should be freed with @ref YoriLibFree when it is no
 longer needed.

 @param Bytes The number of bytes to allocate.

 @param Function Pointer to a constant string indicating the function that is
        allocating the memory.

 @param File Pointer to a constant string indicating the source file that is
        allocating the memory.

 @param Line Specifies the line number within the source file that is
        allocating the memory.

 @return A pointer to the newly allocated memory, or NULL on failure.
 */
PVOID
YoriLibMallocSpecialHeap(
    __in DWORD Bytes,
    __in LPCSTR Function,
    __in LPCSTR File,
    __in DWORD Line
    )
{
    DWORD TotalPagesNeeded;
    PYORI_SPECIAL_HEAP_HEADER Header;
    PYORI_SPECIAL_HEAP_HEADER Commit;
    DWORD StackSize;
    DWORD OldAccess;
#if _M_MRX000
    DWORD Alignment = sizeof(DWORD);
#else
    DWORD Alignment = sizeof(UCHAR);
#endif

    StackSize = 0;
    if (DllKernel32.pRtlCaptureStackBackTrace != NULL) {
        StackSize = sizeof(PVOID) * YORI_SPECIAL_HEAP_STACK_FRAMES;
    }

    TotalPagesNeeded = (Bytes + sizeof(YORI_SPECIAL_HEAP_HEADER) + StackSize + 2 * PAGE_SIZE - 1) / PAGE_SIZE;

    if (YoriLibSpecialHeap.Mutex == NULL) {
        YoriLibSpecialHeap.Mutex = CreateMutex(NULL, FALSE, NULL);
    }

    if (YoriLibSpecialHeap.Mutex == NULL) {
        return NULL;
    }

    if (YoriLibSpecialHeap.ActiveAllocationsList.Next == NULL) {
        YoriLibInitializeListHead(&YoriLibSpecialHeap.ActiveAllocationsList);
    }

    Header = VirtualAlloc(NULL, TotalPagesNeeded * PAGE_SIZE, MEM_RESERVE, PAGE_READWRITE);
    if (Header == NULL) {
        return NULL;
    }

    Commit = VirtualAlloc(Header, TotalPagesNeeded * PAGE_SIZE, MEM_COMMIT, PAGE_READWRITE);
    if (Commit == NULL) {
        VirtualFree(Header, 0, MEM_RELEASE);
        return NULL;
    }

    if (!VirtualProtect((PUCHAR)Header + (TotalPagesNeeded - 1) * PAGE_SIZE, PAGE_SIZE, PAGE_NOACCESS, &OldAccess)) {
        VirtualFree(Header, TotalPagesNeeded * PAGE_SIZE, MEM_DECOMMIT);
        VirtualFree(Header, 0, MEM_RELEASE);
        return NULL;
    }

    FillMemory(Header, (TotalPagesNeeded - 1) * PAGE_SIZE, '@');

    Header->PagesInAllocation = TotalPagesNeeded;
    Header->OffsetToData = ((TotalPagesNeeded - 1) * PAGE_SIZE - Bytes) & ~(Alignment - 1);
    Header->Function = Function;
    Header->File = File;
    Header->Line = Line;
    ASSERT(Header->OffsetToData < (PAGE_SIZE + sizeof(YORI_SPECIAL_HEAP_HEADER) + StackSize));
    if (DllKernel32.pRtlCaptureStackBackTrace != NULL) {
        DllKernel32.pRtlCaptureStackBackTrace(1, YORI_SPECIAL_HEAP_STACK_FRAMES, YoriLibAddToPointer(Header, sizeof(YORI_SPECIAL_HEAP_HEADER)), NULL);
    }

    WaitForSingleObject(YoriLibSpecialHeap.Mutex, INFINITE);
    YoriLibSpecialHeap.NumberAllocated++;
    YoriLibSpecialHeap.BytesCurrentlyAllocated += (Bytes + Alignment - 1) & ~(Alignment - 1);
    YoriLibAppendList(&YoriLibSpecialHeap.ActiveAllocationsList, &Header->ListEntry);
    ReleaseMutex(YoriLibSpecialHeap.Mutex);

    return (PUCHAR)Header + Header->OffsetToData;
}
#endif

/**
 Free memory previously allocated with @ref YoriLibMalloc.

 @param Ptr The memory to free.
 */
VOID
YoriLibFree(
    __in PVOID Ptr
    )
{
#if !YORI_SPECIAL_HEAP
    HeapFree(GetProcessHeap(), 0, Ptr);
#else
    PYORI_SPECIAL_HEAP_HEADER Header;
    PUCHAR TestChar;
    DWORD MyEntry;
    DWORD OldAccess;
    DWORD BytesToFree;
    DWORD StackSize;

    StackSize = 0;
    if (DllKernel32.pRtlCaptureStackBackTrace != NULL) {
        StackSize = sizeof(PVOID) * YORI_SPECIAL_HEAP_STACK_FRAMES;
    }

    Header = YoriLibSubtractFromPointer(Ptr, sizeof(YORI_SPECIAL_HEAP_HEADER) + StackSize);
    Header = (PYORI_SPECIAL_HEAP_HEADER)((DWORD_PTR)Header & ~(PAGE_SIZE - 1));

    TestChar = (PUCHAR)Header;
    ASSERT(TestChar + Header->OffsetToData == Ptr);

    TestChar = (PUCHAR)YoriLibAddToPointer(Header, sizeof(YORI_SPECIAL_HEAP_HEADER) + StackSize);
    while (TestChar < (PUCHAR)Ptr) {
        ASSERT(*TestChar == '@');
        TestChar++;
    }

    BytesToFree = (Header->PagesInAllocation - 1) * PAGE_SIZE - Header->OffsetToData;

    WaitForSingleObject(YoriLibSpecialHeap.Mutex, INFINITE);

    MyEntry = YoriLibSpecialHeap.NumberFreed % (sizeof(YoriLibSpecialHeap.RecentlyFreed)/sizeof(YoriLibSpecialHeap.RecentlyFreed[0]));
    YoriLibSpecialHeap.NumberFreed++;
    YoriLibSpecialHeap.BytesCurrentlyAllocated -= BytesToFree;
    YoriLibRemoveListItem(&Header->ListEntry);

    if (YoriLibSpecialHeap.RecentlyFreed[MyEntry] != NULL) {

        PYORI_SPECIAL_HEAP_HEADER OldHeader;

        OldHeader = YoriLibSpecialHeap.RecentlyFreed[MyEntry];

        if (!VirtualProtect(OldHeader, PAGE_SIZE, PAGE_READWRITE, &OldAccess)) {
            ASSERT(!"VirtualProtect failure");
        }
        if (!VirtualFree(OldHeader, OldHeader->PagesInAllocation * PAGE_SIZE, MEM_DECOMMIT)) {
            ASSERT(!"VirtualFree failure");
        }
        if (!VirtualFree(OldHeader, 0, MEM_RELEASE)) {
            ASSERT(!"VirtualFree failure");
        }
    }

    if (!VirtualProtect(Header, Header->PagesInAllocation * PAGE_SIZE, PAGE_NOACCESS, &OldAccess)) {
        ASSERT(!"VirtualProtect failure");
    }

    YoriLibSpecialHeap.RecentlyFreed[MyEntry] = Header;

    ReleaseMutex(YoriLibSpecialHeap.Mutex);

#endif
}

/**
 When using memory debugging, display the number of bytes of allocation and
 number of allocations currently in use.  When memory debugging is not present,
 does nothing.
 */
VOID
YoriLibDisplayMemoryUsage(VOID)
{
#if YORI_SPECIAL_HEAP
    if (YoriLibSpecialHeap.BytesCurrentlyAllocated > 0 ||
        (YoriLibSpecialHeap.NumberAllocated - YoriLibSpecialHeap.NumberFreed > 0)) {

        PYORI_LIST_ENTRY Entry;
        PYORI_SPECIAL_HEAP_HEADER Header;
        DWORD BytesAllocated;

        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("%i bytes allocated in %i allocations\n"), YoriLibSpecialHeap.BytesCurrentlyAllocated, YoriLibSpecialHeap.NumberAllocated - YoriLibSpecialHeap.NumberFreed);

        Entry = YoriLibGetNextListEntry(&YoriLibSpecialHeap.ActiveAllocationsList, NULL);
        while (Entry != NULL) {
            Header = CONTAINING_RECORD(Entry, YORI_SPECIAL_HEAP_HEADER, ListEntry);
            BytesAllocated = (Header->PagesInAllocation - 1) * PAGE_SIZE - Header->OffsetToData;
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("%hs (%hs:%i) allocated %i bytes\n"), Header->Function, Header->File, Header->Line, BytesAllocated);
            Entry = YoriLibGetNextListEntry(&YoriLibSpecialHeap.ActiveAllocationsList, Entry);
        }

        DebugBreak();
    }
#endif
}


/**
 A structure that preceeds a reference counted malloc allocation.
 */
typedef struct _YORILIB_REFERENCED_MALLOC_HEADER {

    /**
     The number of references on the allocation.
     */
    ULONG ReferenceCount;
} YORILIB_REFERENCED_MALLOC_HEADER, *PYORILIB_REFERENCED_MALLOC_HEADER;

#if !YORI_SPECIAL_HEAP
/**
 Allocate a block of memory that can be reference counted and will be freed
 on final dereference.

 @param Bytes The number of bytes to allocate.

 @return Pointer to the allocated block of memory, or NULL on failure.
 */
PVOID
YoriLibReferencedMalloc(
    __in DWORD Bytes
    )
{
    PYORILIB_REFERENCED_MALLOC_HEADER Header;

    Header = YoriLibMalloc(Bytes + sizeof(YORILIB_REFERENCED_MALLOC_HEADER));
    if (Header == NULL) {
        return NULL;
    }

    Header->ReferenceCount = 1;

    return (PVOID)(Header + 1);
}
#else
/**
 Allocate a block of memory that can be reference counted and will be freed
 on final dereference.

 @param Bytes The number of bytes to allocate.

 @param Function Pointer to a constant string indicating the function that is
        allocating the memory.

 @param File Pointer to a constant string indicating the source file that is
        allocating the memory.

 @param Line Specifies the line number within the source file that is
        allocating the memory.

 @return Pointer to the allocated block of memory, or NULL on failure.
 */
PVOID
YoriLibReferencedMallocSpecialHeap(
    __in DWORD Bytes,
    __in LPCSTR Function,
    __in LPCSTR File,
    __in DWORD Line
    )
{
    PYORILIB_REFERENCED_MALLOC_HEADER Header;

    Header = YoriLibMallocSpecialHeap(Bytes + sizeof(YORILIB_REFERENCED_MALLOC_HEADER), Function, File, Line);
    if (Header == NULL) {
        return NULL;
    }

    Header->ReferenceCount = 1;

    return (PVOID)(Header + 1);
}
#endif

/**
 Reference a previously allocated block of reference counted memory.

 @param Allocation The allocation to add a reference to.
 */
VOID
YoriLibReference(
    __in PVOID Allocation
    )
{
    PYORILIB_REFERENCED_MALLOC_HEADER Header;

    Header = (PYORILIB_REFERENCED_MALLOC_HEADER)Allocation - 1;
    Header->ReferenceCount++;
}

/**
 Dereference a previously allocated block of reference counted memory.  If
 this is the final reference, the block is deallocated.

 @param Allocation The allocation to remove a reference from.
 */
VOID
YoriLibDereference(
    __in PVOID Allocation
    )
{
    PYORILIB_REFERENCED_MALLOC_HEADER Header;

    Header = (PYORILIB_REFERENCED_MALLOC_HEADER)Allocation - 1;
    Header->ReferenceCount--;
    if (Header->ReferenceCount == 0) {
        YoriLibFree(Header);
    }
}


// vim:sw=4:ts=4:et:


<|start_filename|>lib/curdir.c<|end_filename|>
/**
 * @file lib/curdir.c
 *
 * Support for storing and retrieving current directory information.
 *
 * Copyright (c) 2017-2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "yoripch.h"
#include "yorilib.h"

/**
 A global variable of the directory string to display for current directory.
 This value is the result of comparing each component in the path to the on
 disk form to ensure that the case matches objects found on disk.
 */
YORI_STRING YoriLibCurrentDirectoryForDisplay;

/**
 Interrogate each component in the specified path against the file system and
 return a string with the case of each component modified to match objects
 found from disk.  Any component not found or obtainable will be retained as
 is.

 @param Path Pointer to the path to convert.

 @param OnDiskCasePath On successful completion, updated to point to a newly
        allocated string where path components may be updated to refer to
        on disk case.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOL
YoriLibGetOnDiskCaseForPath(
    __in PYORI_STRING Path,
    __out PYORI_STRING OnDiskCasePath
    )
{
    DWORD Index;
    DWORD LastSepOffset;
    HANDLE FindHandle;
    WIN32_FIND_DATA FindData;
    YORI_STRING SearchComponent;
    YORI_STRING NewPath;
    YORI_STRING EffectiveRoot;
    TCHAR LastSepChar;

    if (Path->LengthInChars == 0) {
        YoriLibInitEmptyString(OnDiskCasePath);
        return TRUE;
    }

    if (!YoriLibFindEffectiveRoot(Path, &EffectiveRoot)) {
        return FALSE;
    }

    if (!YoriLibAllocateString(&NewPath, Path->LengthInChars + 1)) {
        return FALSE;
    }

    memcpy(NewPath.StartOfString, Path->StartOfString, Path->LengthInChars * sizeof(TCHAR));
    NewPath.LengthInChars = Path->LengthInChars;
    NewPath.StartOfString[NewPath.LengthInChars] = '\0';

    while (NewPath.LengthInChars > EffectiveRoot.LengthInChars &&
           YoriLibIsSep(NewPath.StartOfString[NewPath.LengthInChars - 1])) {

        NewPath.LengthInChars--;
        NewPath.StartOfString[NewPath.LengthInChars] = '\0';
    }

    LastSepOffset = NewPath.LengthInChars;

    for (Index = NewPath.LengthInChars; Index >= EffectiveRoot.LengthInChars; Index--) {
        if (YoriLibIsSep(NewPath.StartOfString[Index - 1])) {
            SearchComponent.StartOfString = &NewPath.StartOfString[Index];
            SearchComponent.LengthInChars = LastSepOffset - Index;
            if (SearchComponent.LengthInChars > 0) {
                LastSepChar = '\0';
                if (LastSepOffset < NewPath.LengthInChars) {
                    LastSepChar = NewPath.StartOfString[LastSepOffset];
                    NewPath.StartOfString[LastSepOffset] = '\0';
                }
                FindHandle = FindFirstFile(NewPath.StartOfString, &FindData);
                if (FindHandle != INVALID_HANDLE_VALUE) {
                    if (YoriLibCompareStringWithLiteralInsensitive(&SearchComponent, FindData.cFileName) == 0) { 
                        memcpy(SearchComponent.StartOfString, FindData.cFileName, SearchComponent.LengthInChars * sizeof(TCHAR));
                    } else if (YoriLibCompareStringWithLiteralInsensitive(&SearchComponent, FindData.cAlternateFileName) == 0) { 
                        memcpy(SearchComponent.StartOfString, FindData.cAlternateFileName, SearchComponent.LengthInChars * sizeof(TCHAR));
                    }
                    FindClose(FindHandle);
                }
                NewPath.StartOfString[LastSepOffset] = LastSepChar;
            }
            LastSepOffset = Index - 1;
        }
    }

    if (YoriLibIsDriveLetterWithColonAndSlash(&NewPath)) {
        NewPath.StartOfString[0] = YoriLibUpcaseChar(NewPath.StartOfString[0]);
    } else if (YoriLibIsPrefixedDriveLetterWithColonAndSlash(&NewPath)) {
        NewPath.StartOfString[4] = YoriLibUpcaseChar(NewPath.StartOfString[4]);
    }

    memcpy(OnDiskCasePath, &NewPath, sizeof(YORI_STRING));
    return TRUE;
}

/**
 Return the current directory on a drive without changing the current
 directory.

 @param Drive The drive letter whose current directory should be returned.

 @param DriveCurrentDirectory On successful completion, populated with the
        current directory for the specified drive.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOL
YoriLibGetCurrentDirectoryOnDrive(
    __in TCHAR Drive,
    __out PYORI_STRING DriveCurrentDirectory
    )
{
    TCHAR EnvVariableName[4];
    DWORD DriveCurrentDirectoryLength;

    EnvVariableName[0] = '=';
    EnvVariableName[1] = Drive;
    EnvVariableName[2] = ':';
    EnvVariableName[3] = '\0';

    DriveCurrentDirectoryLength = GetEnvironmentVariable(EnvVariableName, NULL, 0);

    if (DriveCurrentDirectoryLength > 0) {
        if (!YoriLibAllocateString(DriveCurrentDirectory, DriveCurrentDirectoryLength)) {
            return FALSE;
        }

        DriveCurrentDirectory->LengthInChars = GetEnvironmentVariable(EnvVariableName, DriveCurrentDirectory->StartOfString, DriveCurrentDirectory->LengthAllocated);
        if (DriveCurrentDirectory->LengthInChars == 0 || DriveCurrentDirectory->LengthInChars >= DriveCurrentDirectoryLength) {
            YoriLibFreeStringContents(DriveCurrentDirectory);
            return FALSE;
        }
    } else {
        if (!YoriLibAllocateString(DriveCurrentDirectory, 4)) {
            return FALSE;
        }
        YoriLibSPrintf(DriveCurrentDirectory->StartOfString, _T("%c:\\"), Drive);
        DriveCurrentDirectory->LengthInChars = 3;
    }
    return TRUE;
}

/**
 Update the current directory on a drive without changing the current
 directory.

 @param Drive The drive letter whose current directory should be updated.

 @param DriveCurrentDirectory The directory to set as the current directory
        on the specified drive.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOL
YoriLibSetCurrentDirectoryOnDrive(
    __in TCHAR Drive,
    __in PYORI_STRING DriveCurrentDirectory
    )
{
    TCHAR EnvVariableName[4];
    LPTSTR szDriveCurrentDirectory;
    BOOL Result;

    EnvVariableName[0] = '=';
    EnvVariableName[1] = Drive;
    EnvVariableName[2] = ':';
    EnvVariableName[3] = '\0';

    szDriveCurrentDirectory = YoriLibMalloc((DriveCurrentDirectory->LengthInChars + 1) * sizeof(TCHAR));
    if (szDriveCurrentDirectory == NULL) {
        return FALSE;
    }

    YoriLibSPrintf(szDriveCurrentDirectory, _T("%y"), DriveCurrentDirectory);

    Result = SetEnvironmentVariable(EnvVariableName, szDriveCurrentDirectory);
    YoriLibFree(szDriveCurrentDirectory);
    return Result;
}

/**
 Allocate a new string and query the current directory from the operating
 system into that allocation.

 @param CurrentDirectory Pointer to a string which will be allocated as part
        of this routine and updated to refer to the process current directory.
        This string will be NULL terminated within this routine.  The caller
        should free this with YoriLibFreeStringContents.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOL
YoriLibGetCurrentDirectory(
    __out PYORI_STRING CurrentDirectory
    )
{
    YORI_STRING String;
    DWORD CharsNeeded;

    CharsNeeded = GetCurrentDirectory(0, NULL);

    while(TRUE) {

        if (!YoriLibAllocateString(&String, CharsNeeded + 1)) {
            return FALSE;
        }
    
    
        String.LengthInChars = GetCurrentDirectory(String.LengthAllocated, String.StartOfString);
    
        if (String.LengthInChars == 0) {
            YoriLibFreeStringContents(&String);
            return FALSE;
        }

        if (String.LengthInChars < String.LengthAllocated) {
            String.StartOfString[String.LengthInChars] = '\0';
            memcpy(CurrentDirectory, &String, sizeof(YORI_STRING));
            return TRUE;
        }

        CharsNeeded = String.LengthInChars;

        YoriLibFreeStringContents(&String);
    }
}

/**
 Return a reference to a string containing the current directory where
 components may have been altered to reflect on disk case.

 @param CurrentDirectory Pointer to a string which will be referenced as part
        of this routine and updated to refer to the process current directory.
        This string will be NULL terminated within this routine.  The caller
        should free this with YoriLibFreeStringContents.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOL
YoriLibGetCurrentDirectoryForDisplay(
    __out PYORI_STRING CurrentDirectory
    )
{
    if (YoriLibCurrentDirectoryForDisplay.LengthInChars == 0) {
        YORI_STRING LocalDir;
        YORI_STRING NewDisplayString;

        if (!YoriLibGetCurrentDirectory(&LocalDir)) {
            return FALSE;
        }

        if (!YoriLibGetOnDiskCaseForPath(&LocalDir, &NewDisplayString)) {
            YoriLibFreeStringContents(&LocalDir);
            return FALSE;
        }

        YoriLibFreeStringContents(&LocalDir);

        YoriLibFreeStringContents(&YoriLibCurrentDirectoryForDisplay);
        memcpy(&YoriLibCurrentDirectoryForDisplay, &NewDisplayString, sizeof(YORI_STRING));
    }

    YoriLibCloneString(CurrentDirectory, &YoriLibCurrentDirectoryForDisplay);
    return TRUE;
}

/**
 Set the current directory for the process.  This will also query the file
 system for on disk case, and internally remember the case corrected name.
 The process is expected to clean up the case corrected name by calling
 @ref YoriLibCleanupCurrentDirectory before terminating.

 @param NewCurrentDirectory Pointer to the directory to set the process
        current directory to.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriLibSetCurrentDirectory(
    __in PYORI_STRING NewCurrentDirectory
    )
{
    YORI_STRING NewDisplayString;
    LPTSTR NullTerminatedDirectory;
    BOOL AllocatedDirectory;
    BOOL Result;
    DWORD LastErr;

    if (!YoriLibGetOnDiskCaseForPath(NewCurrentDirectory, &NewDisplayString)) {
        return FALSE;
    }

    if (YoriLibIsStringNullTerminated(NewCurrentDirectory)) {
        NullTerminatedDirectory = NewCurrentDirectory->StartOfString;
        AllocatedDirectory = FALSE;
    } else {
        NullTerminatedDirectory = YoriLibCStringFromYoriString(NewCurrentDirectory);
        if (NullTerminatedDirectory == NULL) {
            YoriLibFreeStringContents(&NewDisplayString);
            return FALSE;
        }
        AllocatedDirectory = TRUE;
    }


    Result = SetCurrentDirectory(NullTerminatedDirectory);
    LastErr = GetLastError();
    if (AllocatedDirectory) {
        YoriLibDereference(NullTerminatedDirectory);
    }
    if (Result) {
        YoriLibFreeStringContents(&YoriLibCurrentDirectoryForDisplay);
        memcpy(&YoriLibCurrentDirectoryForDisplay, &NewDisplayString, sizeof(YORI_STRING));
    } else {
        YoriLibFreeStringContents(&NewDisplayString);
    }
    SetLastError(LastErr);

    return Result;
}

/**
 Change the current directory to an arbitrary path and modify any per-drive
 current directory if changing to a different drive.

 @param NewCurrentDirectory Pointer to the new directory.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOLEAN
YoriLibSetCurrentDirectorySaveDriveCurrentDirectory(
    __in PYORI_STRING NewCurrentDirectory
    )
{
    YORI_STRING OldCurrentDirectory;
    DWORD OldCurrentDirectoryLength;
    TCHAR NewDrive;
    TCHAR OldDrive;

    ASSERT(YoriLibIsStringNullTerminated(NewCurrentDirectory));

    OldCurrentDirectoryLength = GetCurrentDirectory(0, NULL);
    if (!YoriLibAllocateString(&OldCurrentDirectory, OldCurrentDirectoryLength)) {
        return FALSE;
    }

    OldCurrentDirectory.LengthInChars = GetCurrentDirectory(OldCurrentDirectory.LengthAllocated, OldCurrentDirectory.StartOfString);

    if (OldCurrentDirectory.LengthInChars == 0 ||
        OldCurrentDirectory.LengthInChars >= OldCurrentDirectory.LengthAllocated) {
        YoriLibFreeStringContents(&OldCurrentDirectory);
        return FALSE;
    }

    if (!YoriLibSetCurrentDirectory(NewCurrentDirectory)) {
        YoriLibFreeStringContents(&OldCurrentDirectory);
        return FALSE;
    }

    //
    //  Convert the first character to uppercase for comparison later.
    //

    OldDrive = OldCurrentDirectory.StartOfString[0];
    if (OldDrive >= 'a' && OldDrive <= 'z') {
        OldDrive = YoriLibUpcaseChar(OldDrive);
    }

    NewDrive = NewCurrentDirectory->StartOfString[0];
    if (NewDrive >= 'a' && NewDrive <= 'z') {
        NewDrive = YoriLibUpcaseChar(NewDrive);
    }

    //
    //  If the old current directory is drive letter based, preserve the old
    //  current directory in the environment.
    //

    if (OldDrive >= 'A' &&
        OldDrive <= 'Z' &&
        OldCurrentDirectory.StartOfString[1] == ':') {

        if (NewDrive != OldDrive) {
            YoriLibSetCurrentDirectoryOnDrive(OldDrive, &OldCurrentDirectory);
        }
    }

    YoriLibFreeStringContents(&OldCurrentDirectory);
    return TRUE;
}

/**
 Cleanup any allocated display current directory string.
 */
VOID
YoriLibCleanupCurrentDirectory(VOID)
{
    YoriLibFreeStringContents(&YoriLibCurrentDirectoryForDisplay);
}


// vim:sw=4:ts=4:et:


<|start_filename|>lib/builtin.c<|end_filename|>
/**
 * @file lib/builtin.c
 *
 * Yori routines that are specific to builtin modules
 *
 * Copyright (c) 2017-2019 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "yoripch.h"
#include "yorilib.h"
#include "yoricall.h"

/**
 Retore a set of environment strings into the current environment.  This
 implies removing all currently defined variables and replacing them with
 the specified set.  This version of the routine is specific to builtin
 modules because it manipulates the environment through the YoriCall
 interface.  Note that the input buffer is modified temporarily (ie.,
 it is not immutable.)

 @param NewEnvironment Pointer to the new environment strings to apply.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriLibBuiltinSetEnvironmentStrings(
    __in PYORI_STRING NewEnvironment
    )
{
    YORI_STRING CurrentEnvironment;
    YORI_STRING VariableName;
    YORI_STRING ValueName;
    LPTSTR ThisVar;
    LPTSTR ThisValue;
    DWORD VarLen;

    if (!YoriLibGetEnvironmentStrings(&CurrentEnvironment)) {
        return FALSE;
    }

    YoriLibInitEmptyString(&VariableName);
    YoriLibInitEmptyString(&ValueName);

    ThisVar = CurrentEnvironment.StartOfString;
    while (*ThisVar != '\0') {
        VarLen = _tcslen(ThisVar);

        //
        //  We know there's at least one char.  Skip it if it's equals since
        //  that's how drive current directories are recorded.
        //

        ThisValue = _tcschr(&ThisVar[1], '=');
        if (ThisValue != NULL) {
            ThisValue[0] = '\0';
            VariableName.StartOfString = ThisVar;
            VariableName.LengthInChars = (DWORD)(ThisValue - ThisVar);
            VariableName.LengthAllocated = VariableName.LengthInChars + 1;
            YoriCallSetEnvironmentVariable(&VariableName, NULL);
        }

        ThisVar += VarLen;
        ThisVar++;
    }
    YoriLibFreeStringContents(&CurrentEnvironment);

    //
    //  Now restore the saved environment.
    //

    ThisVar = NewEnvironment->StartOfString;
    while (*ThisVar != '\0') {
        VarLen = _tcslen(ThisVar);

        //
        //  We know there's at least one char.  Skip it if it's equals since
        //  that's how drive current directories are recorded.
        //

        ThisValue = _tcschr(&ThisVar[1], '=');
        if (ThisValue != NULL) {
            ThisValue[0] = '\0';
            VariableName.StartOfString = ThisVar;
            VariableName.LengthInChars = (DWORD)(ThisValue - ThisVar);
            VariableName.LengthAllocated = VariableName.LengthInChars + 1;
            ThisValue++;
            ValueName.StartOfString = ThisValue;
            ValueName.LengthInChars = VarLen - VariableName.LengthInChars - 1;
            ValueName.LengthAllocated = ValueName.LengthInChars + 1;
            YoriCallSetEnvironmentVariable(&VariableName, &ValueName);
            ThisValue--;
            ThisValue[0] = '=';
        }

        ThisVar += VarLen;
        ThisVar++;
    }

    return TRUE;
}

/**
 Normally when running a command if a variable does not define any contents,
 the variable name is preserved in the command.  This does not occur with the
 set or if command, where if a variable contains no contents the variable name
 is removed from the result.  Unfortunately this is ambiguous when the variable
 name is found, because we don't know if the variable was not expanded by the
 shell due to no contents or because it was escaped.  So if it has no contents
 it is removed here.  But this still can't distinguish between an escaped
 variable that points to something and an escaped variable which points to
 nothing, both of which presumably should be retained.

 @param Value Pointer to a string containing the value part of the set
        command (ie., what to set a variable to.)

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriLibBuiltinRemoveEmptyVariables(
    __inout PYORI_STRING Value
    )
{
    DWORD StartOfVariableName;
    DWORD ReadIndex = 0;
    DWORD WriteIndex = 0;

    while (ReadIndex < Value->LengthInChars) {
        if (YoriLibIsEscapeChar(Value->StartOfString[ReadIndex])) {

            //
            //  If the character is an escape, copy it to the destination
            //  and skip the check for the environment variable character
            //  so we fall into the below case and copy the next character
            //  too
            //

            if (ReadIndex != WriteIndex) {
                Value->StartOfString[WriteIndex] = Value->StartOfString[ReadIndex];
            }
            ReadIndex++;
            WriteIndex++;

        } else if (Value->StartOfString[ReadIndex] == '%') {

            //
            //  We found the first %, scan ahead looking for the next
            //  one
            //

            StartOfVariableName = ReadIndex;
            do {
                if (YoriLibIsEscapeChar(Value->StartOfString[ReadIndex])) {
                    ReadIndex++;
                }
                ReadIndex++;
            } while (ReadIndex < Value->LengthInChars && Value->StartOfString[ReadIndex] != '%');

            //
            //  If we found a well formed variable, check if it refers
            //  to anything.  If it does, that means the shell didn't
            //  expand it because it was escaped, so preserve it here.
            //  If it doesn't, that means it may not refer to anything,
            //  so remove it.
            //

            if (ReadIndex < Value->LengthInChars && Value->StartOfString[ReadIndex] == '%') {
                YORI_STRING YsVariableName;
                YORI_STRING YsVariableValue;

                YsVariableName.StartOfString = &Value->StartOfString[StartOfVariableName + 1];
                YsVariableName.LengthInChars = ReadIndex - StartOfVariableName - 1;

                ReadIndex++;
                YoriLibInitEmptyString(&YsVariableValue);
                if (YoriCallGetEnvironmentVariable(&YsVariableName, &YsVariableValue)) {
                    if (WriteIndex != StartOfVariableName) {
                        memmove(&Value->StartOfString[WriteIndex], &Value->StartOfString[StartOfVariableName], (ReadIndex - StartOfVariableName) * sizeof(TCHAR));
                    }
                    WriteIndex += (ReadIndex - StartOfVariableName);
                    YoriCallFreeYoriString(&YsVariableValue);
                }
                continue;
            } else {
                ReadIndex = StartOfVariableName;
            }
        }
        if (ReadIndex != WriteIndex) {
            Value->StartOfString[WriteIndex] = Value->StartOfString[ReadIndex];
        }
        ReadIndex++;
        WriteIndex++;
    }
    Value->LengthInChars = WriteIndex;
    return TRUE;
}



// vim:sw=4:ts=4:et:


<|start_filename|>pkglib/create.c<|end_filename|>
/**
 * @file pkglib/create.c
 *
 * Yori shell create packages
 *
 * Copyright (c) 2018-2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>
#include "yoripkgp.h"

#if defined(_MSC_VER) && (_MSC_VER == 1500)
#pragma warning(push)
#pragma warning(disable: 6309 6387) // GetTempPath is mis-annotated in old SDKs
#endif

/**
 Creates a binary (installable) package.  This could be architecture specific
 or architecture neutral.

 @param FileName The name of the CAB file to create.

 @param PackageName The name of the package described by the CAB file.

 @param Version The version of the package.

 @param Architecture The architecture of the package.

 @param FileListFile A pointer to a file name whose contents describe the list
        of files that should be included in the package.  This file contains
        one file per line, no wildcards.

 @param MinimumOSBuild Optionally points to a string containing the minimum
        build of Windows required to install the package.

 @param PackagePathForOlderBuilds Optionally points to a PackageName for use
        when installing on OS builds below MinimumOSBuild.  If MinimumOSBuild
        is not specified, this value is meaningless.

 @param UpgradePath Optionally points to a URL to upgrade to the latest
        version of the package from.  If not specified, no UpgradePath is
        included in the package.

 @param SourcePath Optionally points to a URL to download source code for the
        package.  If not specified, no SourcePath is included in the package.

 @param SymbolPath Optionally points to a URL to download debugging symbols
        for the package.  If not specified, no SourcePath is included in the
        package.

 @param UpgradeToStablePath Optionally points to a URL to upgrade to the
        latest stable version of the package from.  If not specified, no
        UpgradeToStablePath is included in the package.

 @param UpgradeToDailyPath Optionally points to a URL to upgrade to the
        latest daily version of the package from.  If not specified, no
        UpgradeToDailyPath is included in the package.

 @param Replaces Pointer to an array of strings containing package names that
        this package should replace.  If this package should not replace
        packages with other names, this can be NULL.

 @param ReplaceCount Specifies the number of elements in the Replaces array.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriPkgCreateBinaryPackage(
    __in PYORI_STRING FileName,
    __in PYORI_STRING PackageName,
    __in PYORI_STRING Version,
    __in PYORI_STRING Architecture,
    __in PYORI_STRING FileListFile,
    __in_opt PYORI_STRING MinimumOSBuild,
    __in_opt PYORI_STRING PackagePathForOlderBuilds,
    __in_opt PYORI_STRING UpgradePath,
    __in_opt PYORI_STRING SourcePath,
    __in_opt PYORI_STRING SymbolPath,
    __in_opt PYORI_STRING UpgradeToStablePath,
    __in_opt PYORI_STRING UpgradeToDailyPath,
    __in_ecount_opt(ReplaceCount) PYORI_STRING Replaces,
    __in DWORD ReplaceCount
    )
{
    YORI_STRING TempPath;
    YORI_STRING TempFile;
    YORI_STRING PkgInfoName;
    YORI_STRING LineString;
    YORI_STRING FullFileListFile;
    YORI_STRING FileNameInCab;
    PVOID LineContext = NULL;
    HANDLE FileListSource;
    DWORD Count;

    PVOID CabHandle;

    __analysis_assume(ReplaceCount == 0 || Replaces != NULL);

    //
    //  Query for a temporary directory
    //

    TempPath.LengthAllocated = GetTempPath(0, NULL);
    if (!YoriLibAllocateString(&TempPath, TempPath.LengthAllocated)) {
        return FALSE;
    }
    TempPath.LengthInChars = GetTempPath(TempPath.LengthAllocated, TempPath.StartOfString);

    if (!YoriLibAllocateString(&TempFile, TempPath.LengthAllocated + MAX_PATH)) {
        YoriLibFreeStringContents(&TempPath);
        return FALSE;
    }

    //
    //  Generate a temporary file name to stage pkginfo.ini in to
    //

    if (GetTempFileName(TempPath.StartOfString, _T("ypm"), 0, TempFile.StartOfString) == 0) {
        YoriLibFreeStringContents(&TempPath);
        YoriLibFreeStringContents(&TempFile);
        return FALSE;
    }

    TempFile.LengthInChars = _tcslen(TempFile.StartOfString);
    YoriLibFreeStringContents(&TempPath);

    WritePrivateProfileString(_T("Package"), _T("Name"), PackageName->StartOfString, TempFile.StartOfString);
    WritePrivateProfileString(_T("Package"), _T("Architecture"), Architecture->StartOfString, TempFile.StartOfString);
    WritePrivateProfileString(_T("Package"), _T("Version"), Version->StartOfString, TempFile.StartOfString);
    if (MinimumOSBuild != NULL) {
        WritePrivateProfileString(_T("Package"), _T("MinimumOSBuild"), MinimumOSBuild->StartOfString, TempFile.StartOfString);
        if (PackagePathForOlderBuilds != NULL) {
            WritePrivateProfileString(_T("Package"), _T("PackagePathForOlderBuilds"), PackagePathForOlderBuilds->StartOfString, TempFile.StartOfString);
        }
    }
    if (UpgradePath != NULL) {
        WritePrivateProfileString(_T("Package"), _T("UpgradePath"), UpgradePath->StartOfString, TempFile.StartOfString);
    }
    if (SourcePath != NULL) {
        WritePrivateProfileString(_T("Package"), _T("SourcePath"), SourcePath->StartOfString, TempFile.StartOfString);
    }
    if (SymbolPath != NULL) {
        WritePrivateProfileString(_T("Package"), _T("SymbolPath"), SymbolPath->StartOfString, TempFile.StartOfString);
    }
    if (UpgradeToStablePath != NULL) {
        WritePrivateProfileString(_T("Package"), _T("UpgradeToStablePath"), UpgradeToStablePath->StartOfString, TempFile.StartOfString);
    }
    if (UpgradeToDailyPath != NULL) {
        WritePrivateProfileString(_T("Package"), _T("UpgradeToDailyPath"), UpgradeToDailyPath->StartOfString, TempFile.StartOfString);
    }

    for (Count = 0; Count < ReplaceCount; Count++) {
        WritePrivateProfileString(_T("Replaces"), Replaces[Count].StartOfString, _T("1"), TempFile.StartOfString);
    }

    if (!YoriLibUserStringToSingleFilePath(FileListFile, TRUE, &FullFileListFile)) {
        YoriLibFreeStringContents(&TempFile);
        return FALSE;
    }

    FileListSource = CreateFile(FullFileListFile.StartOfString,
                                GENERIC_READ,
                                FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                                NULL,
                                OPEN_EXISTING,
                                FILE_ATTRIBUTE_NORMAL,
                                NULL);

    if (FileListSource == INVALID_HANDLE_VALUE) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Cannot open %y\n"), &FullFileListFile);
        DeleteFile(TempFile.StartOfString);
        YoriLibFreeStringContents(&TempFile);
        YoriLibFreeStringContents(&FullFileListFile);
        return FALSE;
    }

    YoriLibFreeStringContents(&FullFileListFile);

    if (!YoriLibCreateCab(FileName, &CabHandle)) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("YoriLibCreateCab failure\n"));
        DeleteFile(TempFile.StartOfString);
        YoriLibFreeStringContents(&TempFile);
        CloseHandle(FileListSource);
        return FALSE;
    }
    YoriLibConstantString(&PkgInfoName, _T("pkginfo.ini"));
    if (!YoriLibAddFileToCab(CabHandle, &TempFile, &PkgInfoName)) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("YoriLibAddFileToCab failure\n"));
        DeleteFile(TempFile.StartOfString);
        YoriLibFreeStringContents(&TempFile);
        YoriLibCloseCab(CabHandle);
        CloseHandle(FileListSource);
        return FALSE;
    }

    YoriLibInitEmptyString(&LineString);
    YoriLibInitEmptyString(&FileNameInCab);
    while(TRUE) {
        if (!YoriLibReadLineToString(&LineString, &LineContext, FileListSource)) {
            break;
        }

        FileNameInCab.StartOfString = LineString.StartOfString;
        FileNameInCab.LengthInChars = LineString.LengthInChars;

        //
        //  If there's a pipe character, treat all of the text before it as
        //  the file path to store in the cab, and all of the text after it
        //  as the name to use within the cab.  If there's no pipe character,
        //  the same string is used for both meanings.
        //

        for (Count = 0; Count < LineString.LengthInChars; Count++) {
            if (LineString.StartOfString[Count] == '|') {
                FileNameInCab.StartOfString = &LineString.StartOfString[Count + 1];
                FileNameInCab.LengthInChars = LineString.LengthInChars - Count - 1;

                LineString.LengthInChars = Count;
                break;
            }
        }

        if (!YoriLibAddFileToCab(CabHandle, &LineString, &FileNameInCab)) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("YoriLibAddFileToCab cannot add %y\n"), &LineString);
            DeleteFile(TempFile.StartOfString);
            YoriLibFreeStringContents(&TempFile);
            YoriLibCloseCab(CabHandle);
            YoriLibLineReadClose(LineContext);
            YoriLibFreeStringContents(&LineString);
            CloseHandle(FileListSource);
            return FALSE;
        }

    }

    YoriLibLineReadClose(LineContext);
    CloseHandle(FileListSource);
    YoriLibFreeStringContents(&LineString);
    YoriLibCloseCab(CabHandle);
    DeleteFile(TempFile.StartOfString);
    YoriLibFreeStringContents(&TempFile);

    return TRUE;
}

/**
 A single item to exclude or include.  Note this can refer to multiple files.
 */
typedef struct _YORIPKG_MATCH_ITEM {

    /**
     List of items to match.
     */
    YORI_LIST_ENTRY MatchList;

    /**
     A string describing the object to match, which may include
     wildcards.
     */
    YORI_STRING MatchCriteria;
} YORIPKG_MATCH_ITEM, *PYORIPKG_MATCH_ITEM;

/**
 Context passed between the source package creation operation and every file
 found while creating the source package.
 */
typedef struct _YORIPKG_CREATE_SOURCE_CONTEXT {

    /**
     A handle to the Cabinet being created.
     */
    PVOID CabHandle;

    /**
     Pointer to the name of the package.
     */
    PYORI_STRING PackageName;

    /**
     Pointer to the version of the package.
     */
    PYORI_STRING PackageVersion;

    /**
     A list of criteria to exclude.
     */
    YORI_LIST_ENTRY ExcludeList;

    /**
     A list of criteria to include, even if they have been excluded by the
     ExcludeList.
     */
    YORI_LIST_ENTRY IncludeList;
} YORIPKG_CREATE_SOURCE_CONTEXT, *PYORIPKG_CREATE_SOURCE_CONTEXT;

/**
 Add a new match criteria to the list.

 @param List Pointer to the list to add the match criteria to.

 @param NewCriteria Pointer to the new criteria to add, which may include
        wildcards.
 
 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriPkgCreateSourceAddMatch(
    __in PYORI_LIST_ENTRY List,
    __in PYORI_STRING NewCriteria
    )
{
    PYORIPKG_MATCH_ITEM MatchItem;
    MatchItem = YoriLibReferencedMalloc(sizeof(YORIPKG_MATCH_ITEM) + (NewCriteria->LengthInChars + 1) * sizeof(TCHAR));

    if (MatchItem == NULL) {
        return FALSE;
    }

    ZeroMemory(MatchItem, sizeof(YORIPKG_MATCH_ITEM));
    MatchItem->MatchCriteria.StartOfString = (LPTSTR)(MatchItem + 1);
    MatchItem->MatchCriteria.LengthInChars = NewCriteria->LengthInChars;
    MatchItem->MatchCriteria.LengthAllocated = NewCriteria->LengthInChars + 1;
    memcpy(MatchItem->MatchCriteria.StartOfString, NewCriteria->StartOfString, MatchItem->MatchCriteria.LengthInChars * sizeof(TCHAR));
    MatchItem->MatchCriteria.StartOfString[MatchItem->MatchCriteria.LengthInChars] = '\0';
    YoriLibAppendList(List, &MatchItem->MatchList);
    return TRUE;
}

/**
 Free all previously added exclude or include criteria.

 @param CreateSourceContext Pointer to the create source context to free all
        exclude or include criteria from.
 */
VOID
YoriPkgCreateSourceFreeMatchLists(
    __in PYORIPKG_CREATE_SOURCE_CONTEXT CreateSourceContext
    )
{
    PYORIPKG_MATCH_ITEM MatchItem;
    PYORI_LIST_ENTRY ListEntry;

    ListEntry = YoriLibGetNextListEntry(&CreateSourceContext->ExcludeList, NULL);
    while (ListEntry != NULL) {
        MatchItem = CONTAINING_RECORD(ListEntry, YORIPKG_MATCH_ITEM, MatchList);
        YoriLibRemoveListItem(&MatchItem->MatchList);
        YoriLibDereference(MatchItem);
        ListEntry = YoriLibGetNextListEntry(&CreateSourceContext->ExcludeList, NULL);
    }

    ListEntry = YoriLibGetNextListEntry(&CreateSourceContext->IncludeList, NULL);
    while (ListEntry != NULL) {
        MatchItem = CONTAINING_RECORD(ListEntry, YORIPKG_MATCH_ITEM, MatchList);
        YoriLibRemoveListItem(&MatchItem->MatchList);
        YoriLibDereference(MatchItem);
        ListEntry = YoriLibGetNextListEntry(&CreateSourceContext->IncludeList, NULL);
    }
}

/**
 Returns TRUE to indicate that an object should be excluded based on the
 exclude criteria, or FALSE if it should be included.

 @param CreateSourceContext Pointer to the create source context to check the
        new object against.

 @param RelativeSourcePath Pointer to a string describing the file relative
        to the root of the source of the create operation.

 @return TRUE to exclude the file, FALSE to include it.
 */
BOOL
YoriPkgCreateSourceShouldExclude(
    __in PYORIPKG_CREATE_SOURCE_CONTEXT CreateSourceContext,
    __in PYORI_STRING RelativeSourcePath
    )
{
    PYORIPKG_MATCH_ITEM MatchItem;
    PYORI_LIST_ENTRY ListEntry;

    ListEntry = YoriLibGetNextListEntry(&CreateSourceContext->ExcludeList, NULL);
    while (ListEntry != NULL) {
        MatchItem = CONTAINING_RECORD(ListEntry, YORIPKG_MATCH_ITEM, MatchList);
        if (YoriLibDoesFileMatchExpression(RelativeSourcePath, &MatchItem->MatchCriteria)) {

            ListEntry = YoriLibGetNextListEntry(&CreateSourceContext->IncludeList, NULL);
            while (ListEntry != NULL) {
                MatchItem = CONTAINING_RECORD(ListEntry, YORIPKG_MATCH_ITEM, MatchList);
                if (YoriLibDoesFileMatchExpression(RelativeSourcePath, &MatchItem->MatchCriteria)) {
                    return FALSE;
                }
                ListEntry = YoriLibGetNextListEntry(&CreateSourceContext->IncludeList, ListEntry);
            }
            return TRUE;
        }
        ListEntry = YoriLibGetNextListEntry(&CreateSourceContext->ExcludeList, ListEntry);
    }
    return FALSE;
}

/**
 A callback that is invoked when a file is found within the tree root that is
 being turned into a source package.

 @param FilePath Pointer to the file path that was found.

 @param FileInfo Information about the file.

 @param Depth Indicates the recursion depth.  Used to determine the relative
        path to check if an object should be included or excluded.

 @param Context Pointer to a context block specifying the destination of 
        package.

 @return TRUE to continute enumerating, FALSE to abort.
 */
BOOL
YoriPkgCreateSourceFileFoundCallback(
    __in PYORI_STRING FilePath,
    __in PWIN32_FIND_DATA FileInfo,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    PYORIPKG_CREATE_SOURCE_CONTEXT CreateSourceContext = (PYORIPKG_CREATE_SOURCE_CONTEXT)Context;
    YORI_STRING RelativePathFromSource;
    YORI_STRING PathInCab;
    DWORD SlashesFound;
    DWORD Index;

    UNREFERENCED_PARAMETER(FileInfo);

    ASSERT(YoriLibIsStringNullTerminated(FilePath));

    YoriLibInitEmptyString(&RelativePathFromSource);

    SlashesFound = 0;
    for (Index = FilePath->LengthInChars; Index > 0; Index--) {
        if (FilePath->StartOfString[Index - 1] == '\\') {
            SlashesFound++;
            if (SlashesFound == Depth + 1) {
                break;
            }
        }
    }

    ASSERT(Index > 0);
    ASSERT(SlashesFound == Depth + 1);

    RelativePathFromSource.StartOfString = &FilePath->StartOfString[Index];
    RelativePathFromSource.LengthInChars = FilePath->LengthInChars - Index;

    //
    //  Skip any object starting with .git or .svn
    //

    if (YoriLibCompareStringWithLiteralCount(&RelativePathFromSource, _T(".git"), sizeof(".git") - 1) == 0 ||
        YoriLibCompareStringWithLiteralCount(&RelativePathFromSource, _T(".svn"), sizeof(".svn") - 1) == 0) {

        return TRUE;
    }

    //
    //  Skip anything .gitinclude said should be skipped
    //

    if (YoriPkgCreateSourceShouldExclude(CreateSourceContext, &RelativePathFromSource)) {
        return TRUE;
    }

    YoriLibInitEmptyString(&PathInCab);
    YoriLibYPrintf(&PathInCab, _T("src\\%y-%y\\%y"), CreateSourceContext->PackageName, CreateSourceContext->PackageVersion, &RelativePathFromSource);

    if (!YoriLibAddFileToCab(CreateSourceContext->CabHandle, FilePath, &PathInCab)) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("YoriLibAddFileToCab cannot add %y\n"), &RelativePathFromSource);
    }
    YoriLibFreeStringContents(&PathInCab);

    return TRUE;
}

/**
 A callback that is invoked when a directory cannot be successfully enumerated.

 @param FilePath Pointer to the file path that could not be enumerated.

 @param ErrorCode The Win32 error code describing the failure.

 @param Depth Recursion depth, ignored in this application.

 @param Context Context, ignored in this function.

 @return TRUE to continute enumerating, FALSE to abort.
 */
BOOL
YoriPkgCreateSourceEnumerateErrorCallback(
    __in PYORI_STRING FilePath,
    __in DWORD ErrorCode,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    YORI_STRING UnescapedFilePath;
    BOOL Result = FALSE;

    UNREFERENCED_PARAMETER(Depth);
    UNREFERENCED_PARAMETER(Context);

    YoriLibInitEmptyString(&UnescapedFilePath);
    if (!YoriLibUnescapePath(FilePath, &UnescapedFilePath)) {
        UnescapedFilePath.StartOfString = FilePath->StartOfString;
        UnescapedFilePath.LengthInChars = FilePath->LengthInChars;
    }

    if (ErrorCode == ERROR_FILE_NOT_FOUND || ErrorCode == ERROR_PATH_NOT_FOUND) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("File or directory not found: %y\n"), &UnescapedFilePath);
    } else {
        LPTSTR ErrText = YoriLibGetWinErrorText(ErrorCode);
        YORI_STRING DirName;
        LPTSTR FilePart;
        YoriLibInitEmptyString(&DirName);
        DirName.StartOfString = UnescapedFilePath.StartOfString;
        FilePart = YoriLibFindRightMostCharacter(&UnescapedFilePath, '\\');
        if (FilePart != NULL) {
            DirName.LengthInChars = (DWORD)(FilePart - DirName.StartOfString);
        } else {
            DirName.LengthInChars = UnescapedFilePath.LengthInChars;
        }
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Enumerate of %y failed: %s"), &DirName, ErrText);
        YoriLibFreeWinErrorText(ErrText);
    }
    YoriLibFreeStringContents(&UnescapedFilePath);
    return Result;
}


/**
 Creates a source package.  This is intrinsically architecture neutral and is
 comprised of a directory tree rather than a file list.

 @param FileName The name of the CAB file to create.

 @param PackageName The name of the package described by the CAB file.

 @param Version The version of the package.

 @param FileRoot A pointer to a file tree root that contains the source tree.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriPkgCreateSourcePackage(
    __in PYORI_STRING FileName,
    __in PYORI_STRING PackageName,
    __in PYORI_STRING Version,
    __in PYORI_STRING FileRoot
    )
{
    YORI_STRING TempPath;
    YORI_STRING TempFile;
    YORI_STRING PkgInfoName;
    YORI_STRING ExcludeFilePath;
    YORIPKG_CREATE_SOURCE_CONTEXT CreateSourceContext;

    ZeroMemory(&CreateSourceContext, sizeof(CreateSourceContext));
    YoriLibInitializeListHead(&CreateSourceContext.ExcludeList);
    YoriLibInitializeListHead(&CreateSourceContext.IncludeList);

    //
    //  Query for a temporary directory
    //

    TempPath.LengthAllocated = GetTempPath(0, NULL);
    if (!YoriLibAllocateString(&TempPath, TempPath.LengthAllocated)) {
        return FALSE;
    }
    TempPath.LengthInChars = GetTempPath(TempPath.LengthAllocated, TempPath.StartOfString);

    if (!YoriLibAllocateString(&TempFile, TempPath.LengthAllocated + MAX_PATH)) {
        YoriLibFreeStringContents(&TempPath);
        return FALSE;
    }

    //
    //  Generate a temporary file name to stage pkginfo.ini in to
    //

    if (GetTempFileName(TempPath.StartOfString, _T("ypm"), 0, TempFile.StartOfString) == 0) {
        YoriLibFreeStringContents(&TempPath);
        YoriLibFreeStringContents(&TempFile);
        return FALSE;
    }

    TempFile.LengthInChars = _tcslen(TempFile.StartOfString);
    YoriLibFreeStringContents(&TempPath);

    WritePrivateProfileString(_T("Package"), _T("Name"), PackageName->StartOfString, TempFile.StartOfString);
    WritePrivateProfileString(_T("Package"), _T("Version"), Version->StartOfString, TempFile.StartOfString);
    WritePrivateProfileString(_T("Package"), _T("Architecture"), _T("noarch"), TempFile.StartOfString);

    if (!YoriLibCreateCab(FileName, &CreateSourceContext.CabHandle)) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("YoriLibCreateCab failure\n"));
        DeleteFile(TempFile.StartOfString);
        YoriLibFreeStringContents(&TempFile);
        return FALSE;
    }
    YoriLibConstantString(&PkgInfoName, _T("pkginfo.ini"));
    if (!YoriLibAddFileToCab(CreateSourceContext.CabHandle, &TempFile, &PkgInfoName)) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("YoriLibAddFileToCab failure\n"));
        DeleteFile(TempFile.StartOfString);
        YoriLibFreeStringContents(&TempFile);
        YoriLibCloseCab(CreateSourceContext.CabHandle);
        return FALSE;
    }

    DeleteFile(TempFile.StartOfString);
    YoriLibFreeStringContents(&TempFile);

    YoriLibInitEmptyString(&ExcludeFilePath);
    YoriLibYPrintf(&ExcludeFilePath, _T("%y\\.gitignore"), FileRoot);
    if (ExcludeFilePath.StartOfString != NULL) {
        HANDLE ExcludeFileHandle;
        YORI_STRING LineString;
        PVOID LineContext = NULL;
        DWORD Index;

        ExcludeFileHandle = CreateFile(ExcludeFilePath.StartOfString,
                                       GENERIC_READ,
                                       FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                                       NULL,
                                       OPEN_EXISTING,
                                       FILE_ATTRIBUTE_NORMAL,
                                       NULL);

        if (ExcludeFileHandle != INVALID_HANDLE_VALUE) {
            YoriLibInitEmptyString(&LineString);
            while(TRUE) {
                if (!YoriLibReadLineToString(&LineString, &LineContext, ExcludeFileHandle)) {
                    break;
                }

                if (LineString.LengthInChars > 0) {

                    for (Index = 0; Index < LineString.LengthInChars; Index++) {
                        if (YoriLibIsSep(LineString.StartOfString[Index])) {
                            LineString.StartOfString[Index] = '\\';
                        }
                    }

                    if (LineString.StartOfString[0] == '!') {
                        LineString.StartOfString++;
                        LineString.LengthInChars--;
                        if (LineString.LengthInChars > 0) {
                            YoriPkgCreateSourceAddMatch(&CreateSourceContext.IncludeList, &LineString);
                        }
                        LineString.StartOfString--;
                        LineString.LengthInChars++;
                    } else {
                        YoriPkgCreateSourceAddMatch(&CreateSourceContext.ExcludeList, &LineString);
                    }
                }
            }
            YoriLibLineReadClose(LineContext);
            YoriLibFreeStringContents(&LineString);
            CloseHandle(ExcludeFileHandle);
        }
    }
    YoriLibFreeStringContents(&ExcludeFilePath);

    CreateSourceContext.PackageName = PackageName;
    CreateSourceContext.PackageVersion = Version;

    YoriLibForEachFile(FileRoot,
                       YORILIB_FILEENUM_RETURN_FILES | YORILIB_FILEENUM_DIRECTORY_CONTENTS | YORILIB_FILEENUM_RECURSE_AFTER_RETURN | YORILIB_FILEENUM_NO_LINK_TRAVERSE,
                       0,
                       YoriPkgCreateSourceFileFoundCallback,
                       YoriPkgCreateSourceEnumerateErrorCallback,
                       &CreateSourceContext);

    YoriLibCloseCab(CreateSourceContext.CabHandle);
    YoriPkgCreateSourceFreeMatchLists(&CreateSourceContext);
    return TRUE;
}

#if defined(_MSC_VER) && (_MSC_VER == 1500)
#pragma warning(pop)
#endif


// vim:sw=4:ts=4:et:


<|start_filename|>lib/progman.c<|end_filename|>
/**
 * @file lib/progman.c
 *
 * Yori Program Manager DDE interfaces
 *
 * Copyright (c) 2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "yoripch.h"
#include "yorilib.h"

/**
 Pointless callback because DDEML expects it.

 @param Type Ignored.

 @param Fmt Ignored.

 @param ConvHandle Ignored.

 @param StringHandle1 Ignored.

 @param StringHandle2 Ignored.

 @param DataHandle Ignored.

 @param Data1 Ignored.

 @param Data2 Ignored.

 @return NULL.
 */
HDDEDATA CALLBACK
YoriLibDdeCallback(
    __in DWORD Type,
    __in DWORD Fmt,
    __in HCONV ConvHandle,
    __in HSZ StringHandle1,
    __in HSZ StringHandle2,
    __in HDDEDATA DataHandle,
    __in DWORD_PTR Data1,
    __in DWORD_PTR Data2
    )
{
    UNREFERENCED_PARAMETER(Type);
    UNREFERENCED_PARAMETER(Fmt);
    UNREFERENCED_PARAMETER(ConvHandle);
    UNREFERENCED_PARAMETER(StringHandle1);
    UNREFERENCED_PARAMETER(StringHandle2);
    UNREFERENCED_PARAMETER(DataHandle);
    UNREFERENCED_PARAMETER(Data1);
    UNREFERENCED_PARAMETER(Data2);
    return (HDDEDATA)NULL;
}

/**
 Check if the string contains characters which would need to be escaped to
 communicate over DDE.

 @param String Pointer to the string to check.

 @return TRUE to indicate the string contains invalid chars, FALSE if it
         does not.
 */
BOOL
YoriLibDoesStringContainDDEInvalidChars(
    __in PCYORI_STRING String
    )
{
    DWORD Index;

    //
    //  A string better not contain an argument seperator or command
    //  terminator character.
    //

    for (Index = 0; Index < String->LengthInChars; Index++) {
        if (String->StartOfString[Index] == ',' ||
            String->StartOfString[Index] == ')') {

            return TRUE;
        }
    }

    return FALSE;
}

/**
 Create or modify a Program Manager item.

 @param GroupName Pointer to the name of the Program Manager group.

 @param ItemName The name of the Program Manager item.

 @param ItemPath The path that the Program Manager item should point to.

 @param WorkingDirectory If specified, the current directory to set when
        launching the executable.

 @param IconPath If specified, the path to the binary containing the icon for
        the shortcut.

 @param IconIndex The index of the icon within any executable or DLL used as
        the source of the icon.  This is ignored unless IconPath is specified.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOL
YoriLibAddProgmanItem(
    __in PCYORI_STRING GroupName,
    __in PCYORI_STRING ItemName,
    __in PCYORI_STRING ItemPath,
    __in_opt PCYORI_STRING WorkingDirectory,
    __in_opt PCYORI_STRING IconPath,
    __in DWORD IconIndex
    )
{
    HSZ ProgmanHandle;
    HDDEDATA CommandHandle;
    HCONV ConvHandle;
    DWORD DdeInstance;
    DWORD Result;
    CONVCONTEXT ConvContext;
    YORI_STRING CommandString;

    //
    //  Check for characters that can't be communicated over DDE.  There's an
    //  escaping protocol for these, but it hardly seems worth implementing
    //  given that this function is very unlikely to see these characters.
    //

    if (YoriLibDoesStringContainDDEInvalidChars(GroupName) ||
        YoriLibDoesStringContainDDEInvalidChars(ItemName) ||
        YoriLibDoesStringContainDDEInvalidChars(ItemPath)) {

        return FALSE;
    }

    if (WorkingDirectory != NULL && YoriLibDoesStringContainDDEInvalidChars(WorkingDirectory)) {
        return FALSE;
    }

    if (IconPath != NULL && YoriLibDoesStringContainDDEInvalidChars(IconPath)) {
        return FALSE;
    }

    YoriLibLoadUser32Functions();

    if (DllUser32.pDdeClientTransaction == NULL ||
        DllUser32.pDdeConnect == NULL ||
        DllUser32.pDdeCreateDataHandle == NULL ||
        DllUser32.pDdeCreateStringHandleW == NULL ||
        DllUser32.pDdeDisconnect == NULL ||
        DllUser32.pDdeFreeStringHandle == NULL ||
        DllUser32.pDdeInitializeW == NULL ||
        DllUser32.pDdeUninitialize == NULL) {

        return FALSE;
    }

    //
    //  Initialize DDE and connect to Program Manager.
    //

    DdeInstance = 0;
    if (DllUser32.pDdeInitializeW(&DdeInstance, (PFNCALLBACK)YoriLibDdeCallback, APPCMD_CLIENTONLY, 0) != DMLERR_NO_ERROR) {
        return FALSE;
    }

    ProgmanHandle = DllUser32.pDdeCreateStringHandleW(DdeInstance, _T("PROGMAN"), CP_WINUNICODE);
    if (ProgmanHandle == (HSZ)NULL) {
        DllUser32.pDdeUninitialize(DdeInstance);
        return FALSE;
    }

    ZeroMemory(&ConvContext, sizeof(ConvContext));
    ConvContext.cb = sizeof(ConvContext);
    ConvHandle = DllUser32.pDdeConnect(DdeInstance, ProgmanHandle, ProgmanHandle, &ConvContext);
    DllUser32.pDdeFreeStringHandle(DdeInstance, ProgmanHandle);
    if (ConvHandle == (HCONV)NULL) {
        DllUser32.pDdeUninitialize(DdeInstance);
        return FALSE;
    }

    //
    //  Create or switch to the group.
    //

    YoriLibInitEmptyString(&CommandString);
    YoriLibYPrintf(&CommandString, _T("[CreateGroup(%y)]"), GroupName);
    if (CommandString.StartOfString == NULL) {
        DllUser32.pDdeDisconnect(ConvHandle);
        DllUser32.pDdeUninitialize(DdeInstance);
        return FALSE;
    }

    CommandHandle = DllUser32.pDdeCreateDataHandle(DdeInstance, (LPBYTE)CommandString.StartOfString, (CommandString.LengthInChars + 1) * sizeof(TCHAR), 0, (HSZ)NULL, CF_UNICODETEXT, 0);
    if (CommandHandle == (HDDEDATA)NULL) {
        YoriLibFreeStringContents(&CommandString);
        DllUser32.pDdeDisconnect(ConvHandle);
        DllUser32.pDdeUninitialize(DdeInstance);
        return FALSE;
    }

    if (DllUser32.pDdeClientTransaction((LPBYTE)CommandHandle, 0xFFFFFFFF, ConvHandle, (HSZ)NULL, 0, XTYP_EXECUTE, 3000, &Result) == (HDDEDATA)NULL) {
        //
        //  Note that CommandHandle may be leaked here.  Avoiding this
        //  requires establishing what happened (whether it made it to
        //  the remote side or not.)
        //
        YoriLibFreeStringContents(&CommandString);
        DllUser32.pDdeDisconnect(ConvHandle);
        DllUser32.pDdeUninitialize(DdeInstance);
        return FALSE;
    }

    //
    //  At this point CommandHandle is owned by the remote side.
    //
    //  Create the program item.
    //

    CommandString.LengthInChars = 0;
    if (IconPath != NULL && WorkingDirectory != NULL) {
        YoriLibYPrintf(&CommandString, _T("[AddItem(\"%y\",%y,\"%y\",%i,-1,-1,\"%y\")]"), ItemPath, ItemName, IconPath, IconIndex, WorkingDirectory);
    } else if (WorkingDirectory != NULL) {
        YoriLibYPrintf(&CommandString, _T("[AddItem(\"%y\",%y,,,-1,-1,\"%y\")]"), ItemPath, ItemName, WorkingDirectory);
    } else if (IconPath != NULL) {
        YoriLibYPrintf(&CommandString, _T("[AddItem(\"%y\",%y,\"%y\",%i)]"), ItemPath, ItemName, IconPath, IconIndex);
    } else {
        YoriLibYPrintf(&CommandString, _T("[AddItem(\"%y\",%y)]"), ItemPath, ItemName);
    }

    if (CommandString.LengthInChars == 0) {
        YoriLibFreeStringContents(&CommandString);
        DllUser32.pDdeDisconnect(ConvHandle);
        DllUser32.pDdeUninitialize(DdeInstance);
        return FALSE;
    }

    CommandHandle = DllUser32.pDdeCreateDataHandle(DdeInstance, (LPBYTE)CommandString.StartOfString, (CommandString.LengthInChars + 1) * sizeof(TCHAR), 0, (HSZ)NULL, CF_UNICODETEXT, 0);
    if (CommandHandle == (HDDEDATA)NULL) {
        YoriLibFreeStringContents(&CommandString);
        DllUser32.pDdeDisconnect(ConvHandle);
        DllUser32.pDdeUninitialize(DdeInstance);
        return FALSE;
    }

    if (DllUser32.pDdeClientTransaction((LPBYTE)CommandHandle, 0xFFFFFFFF, ConvHandle, (HSZ)NULL, 0, XTYP_EXECUTE, 3000, &Result) == (HDDEDATA)NULL) {
        //
        //  Note that CommandHandle may be leaked here.  Avoiding this
        //  requires establishing what happened (whether it made it to
        //  the remote side or not.)
        //
        YoriLibFreeStringContents(&CommandString);
        DllUser32.pDdeDisconnect(ConvHandle);
        DllUser32.pDdeUninitialize(DdeInstance);
        return FALSE;
    }

    YoriLibFreeStringContents(&CommandString);
    DllUser32.pDdeDisconnect(ConvHandle);
    DllUser32.pDdeUninitialize(DdeInstance);

    return TRUE;
}


// vim:sw=4:ts=4:et:


<|start_filename|>lsof/lsof.c<|end_filename|>
/**
 * @file lsof/lsof.c
 *
 * Yori determine which processes are keeping files open.
 *
 * Copyright (c) 2018-2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 Help text to display to the user.
 */
const
CHAR strLsofHelpText[] =
        "\n"
        "Determine which processes are keeping files open.\n"
        "\n"
        "LSOF [-license] [-b] [-s] <file>...\n"
        "\n"
        "   -b             Use basic search criteria for files only\n"
        "   -s             Process files from all subdirectories\n";

/**
 Display usage text to the user.
 */
BOOL
LsofHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Lsof %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strLsofHelpText);
    return TRUE;
}

/**
 Context passed to the callback which is invoked for each file found.
 */
typedef struct _LSOF_CONTEXT {

    /**
     Counts the number of files processed in an enumerate.  If this is zero,
     the program assumes the request is to create a new file.
     */
    DWORD FilesFoundThisArg;

    /**
     The number of bytes in the Buffer below.
     */
    DWORD BufferLength;

    /**
     A buffer that is populated with the array of process IDs using a given
     file.
     */
    PFILE_PROCESS_IDS_USING_FILE_INFORMATION Buffer;

} LSOF_CONTEXT, *PLSOF_CONTEXT;

/**
 A callback that is invoked when a file is found that matches a search criteria
 specified in the set of strings to enumerate.

 @param FilePath Pointer to the file path that was found.

 @param FileInfo Information about the file.  Note in this application this can
        be NULL when it is operating on files that do not yet exist.

 @param Depth Specifies the recursion depth.  Ignored in this application.

 @param Context Pointer to the lsof context structure indicating the
        action to perform and populated with the file and line count found.

 @return TRUE to continute enumerating, FALSE to abort.
 */
BOOL
LsofFileFoundCallback(
    __in PYORI_STRING FilePath,
    __in_opt PWIN32_FIND_DATA FileInfo,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    HANDLE FileHandle;
    PLSOF_CONTEXT LsofContext = (PLSOF_CONTEXT)Context;
    IO_STATUS_BLOCK IoStatus;
    DWORD Index;
    LONG Status;

    UNREFERENCED_PARAMETER(Depth);
    UNREFERENCED_PARAMETER(FileInfo);

    ASSERT(YoriLibIsStringNullTerminated(FilePath));

    LsofContext->FilesFoundThisArg++;

    FileHandle = CreateFile(FilePath->StartOfString,
                            FILE_READ_ATTRIBUTES,
                            FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                            NULL,
                            OPEN_EXISTING,
                            FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS,
                            NULL);

    if (FileHandle == NULL || FileHandle == INVALID_HANDLE_VALUE) {
        DWORD LastError = GetLastError();
        if (LastError == ERROR_ACCESS_DENIED &&
            DllNtDll.pRtlGetLastNtStatus != NULL &&
            DllNtDll.pRtlGetLastNtStatus() == (LONG)0xC0000056) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("lsof: open of %y failed: the file is delete pending\n"), FilePath);
        } else {
            LPTSTR ErrText = YoriLibGetWinErrorText(LastError);
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("lsof: open of %y failed: %s"), FilePath, ErrText);
            YoriLibFreeWinErrorText(ErrText);
        }
        return TRUE;
    }

    Status = DllNtDll.pNtQueryInformationFile(FileHandle, &IoStatus, LsofContext->Buffer, LsofContext->BufferLength, FileProcessIdsUsingFileInformation);
    if (Status == 0) {
        for (Index = 0; Index < LsofContext->Buffer->NumberOfProcesses; Index++) {
            HANDLE ProcessHandle;
            TCHAR ProcessName[300];
            DWORD ProcessNameSize;

            ProcessName[0] = '\0';
            ProcessNameSize = sizeof(ProcessName)/sizeof(ProcessName[0]);
            ProcessHandle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, (DWORD)LsofContext->Buffer->ProcessIds[Index]);
            if (ProcessHandle != NULL) {
                DllKernel32.pQueryFullProcessImageNameW(ProcessHandle, 0, ProcessName, &ProcessNameSize);
            }
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%10i %s\n"), LsofContext->Buffer->ProcessIds[Index], ProcessName);
        }
    } else {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("lsof: query of %y failed: %08x"), FilePath, Status);
    }

    CloseHandle(FileHandle);
    return TRUE;
}

/**
 Display information about handles opened for all processes.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOLEAN
LsofDumpHandles(VOID)
{
    PYORI_SYSTEM_HANDLE_INFORMATION_EX Handles;
    DWORD_PTR Index;
    PYORI_SYSTEM_HANDLE_ENTRY_EX ThisHandle;
    HANDLE LocalProcessHandle;
    PYORI_OBJECT_NAME_INFORMATION ObjectName;
    PYORI_OBJECT_TYPE_INFORMATION ObjectType;
    YORI_STRING ModuleNameString;
    YORI_STRING ObjectNameString;
    YORI_STRING ObjectTypeString;
    DWORD ObjectNameLength;
    DWORD ObjectTypeLength;
    DWORD LengthReturned;
    HANDLE ProcessHandle;
    DWORD LastPid;

    ProcessHandle = INVALID_HANDLE_VALUE;
    LastPid = 0;

    YoriLibLoadPsapiFunctions();

    if (!YoriLibGetSystemHandlesList(&Handles)) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("lsof: Error getting system handle list\n"));
        return FALSE;
    }

    ObjectNameLength = 0x10000;
    ObjectName = YoriLibMalloc(ObjectNameLength);
    if (ObjectName == NULL) {
        YoriLibFree(Handles);
        return FALSE;
    }

    ObjectTypeLength = 0x1000;
    ObjectType = YoriLibMalloc(ObjectTypeLength);
    if (ObjectName == NULL) {
        YoriLibFree(ObjectName);
        YoriLibFree(Handles);
        return FALSE;
    }

    if (!YoriLibAllocateString(&ModuleNameString, 0x8000)) {
        YoriLibFree(ObjectType);
        YoriLibFree(ObjectName);
        YoriLibFree(Handles);
        return FALSE;
    }

    for (Index = 0; Index < Handles->NumberOfHandles; Index++) {
        ThisHandle = &Handles->Handles[Index];
        if (ProcessHandle == INVALID_HANDLE_VALUE ||
            LastPid != ThisHandle->ProcessId) {

            if (ProcessHandle != INVALID_HANDLE_VALUE &&
                ProcessHandle != NULL) {

                CloseHandle(ProcessHandle);
            }

            //
            //  This open may fail.  If it does, we can't get information
            //  about this process, which makes displaying numeric values
            //  pointless.  Leaving this value as NULL is used to suppress
            //  processing this process, but when we get to the next
            //  process (LastPid != ThisHandle->ProcessId), the next
            //  process will be opened.
            //

            ProcessHandle = OpenProcess(PROCESS_DUP_HANDLE | PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, (DWORD)ThisHandle->ProcessId);
            if (ProcessHandle == NULL) {
                ProcessHandle = OpenProcess(PROCESS_DUP_HANDLE, FALSE, (DWORD)ThisHandle->ProcessId);
            }
            LastPid = (DWORD)ThisHandle->ProcessId;

            ModuleNameString.LengthInChars = 0;
            if (DllPsapi.pGetModuleFileNameExW != NULL &&
                ProcessHandle != NULL) {

                ModuleNameString.LengthInChars = DllPsapi.pGetModuleFileNameExW(ProcessHandle, NULL, ModuleNameString.StartOfString, ModuleNameString.LengthAllocated);
            }

            if (ProcessHandle == NULL) {
                YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Process %i ** NO ACCESS **\n"), LastPid);
            } else {
                YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Process %i %y\n"), LastPid, &ModuleNameString);
            }
        }

        if (ProcessHandle != NULL) {

            ObjectName->Name.LengthInBytes = 0;
            ObjectType->TypeName.LengthInBytes = 0;

            //
            //  Get a local instance of the handle and see what information
            //  can be extracted from them
            //

            LocalProcessHandle = INVALID_HANDLE_VALUE;
            if (DuplicateHandle(ProcessHandle, (HANDLE)ThisHandle->HandleValue, GetCurrentProcess(), &LocalProcessHandle, 0, FALSE, DUPLICATE_SAME_ACCESS)) {
                DllNtDll.pNtQueryObject(LocalProcessHandle, 1, ObjectName, ObjectNameLength, &LengthReturned);
                DllNtDll.pNtQueryObject(LocalProcessHandle, 2, ObjectType, ObjectTypeLength, &LengthReturned);
            } else {
                LocalProcessHandle = INVALID_HANDLE_VALUE;
            }

            //
            //  Convert any output into strings for display
            //

            YoriLibInitEmptyString(&ObjectNameString);
            if (ObjectName->Name.LengthInBytes > 0) {
                ObjectNameString.LengthInChars = ObjectName->Name.LengthInBytes / sizeof(WCHAR);
                ObjectNameString.StartOfString = ObjectName->Name.Buffer;
            }
            YoriLibInitEmptyString(&ObjectTypeString);
            if (ObjectType->TypeName.LengthInBytes > 0) {
                ObjectTypeString.LengthInChars = ObjectType->TypeName.LengthInBytes / sizeof(WCHAR);
                ObjectTypeString.StartOfString = ObjectType->TypeName.Buffer;
            }

            //
            //  Only display files, since that's part of the point of the
            //  program.
            //

            if (YoriLibCompareStringWithLiteralInsensitive(&ObjectTypeString, _T("File")) == 0) {

                PYORI_STRING NameToDisplay;

                NameToDisplay = &ObjectNameString;

                //
                //  If it's possible to get a Win32 path name, display that.
                //  Otherwise, use what we have.
                //

                ModuleNameString.LengthInChars = 0;
                if (DllKernel32.pGetFinalPathNameByHandleW != NULL) {

                    ModuleNameString.LengthInChars = DllKernel32.pGetFinalPathNameByHandleW(LocalProcessHandle, ModuleNameString.StartOfString, ModuleNameString.LengthAllocated, 0);

                    if (ModuleNameString.LengthInChars > 0 &&
                        ModuleNameString.LengthInChars < ModuleNameString.LengthAllocated) {
                        NameToDisplay = &ModuleNameString;
                    }
                }


                YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Handle %lli Object %p  %y\n"), ThisHandle->HandleValue, ThisHandle->Object, NameToDisplay);
            }

            if (LocalProcessHandle != INVALID_HANDLE_VALUE) {
                CloseHandle(LocalProcessHandle);
            }
        }
    }

    YoriLibFreeStringContents(&ModuleNameString);
    YoriLibFree(ObjectType);
    YoriLibFree(ObjectName);
    YoriLibFree(Handles);
    return TRUE;
}

#ifdef YORI_BUILTIN
/**
 The main entrypoint for the lsof builtin command.
 */
#define ENTRYPOINT YoriCmd_LSOF
#else
/**
 The main entrypoint for the lsof standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the lsof cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the child process on success, or failure if the child
         could not be launched.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    DWORD MatchFlags;
    BOOL Recursive = FALSE;
    BOOL BasicEnumeration = FALSE;
    LSOF_CONTEXT LsofContext;
    YORI_STRING Arg;

    ZeroMemory(&LsofContext, sizeof(LsofContext));

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                LsofHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2018-2021"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("b")) == 0) {
                BasicEnumeration = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("s")) == 0) {
                Recursive = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                StartArg = i + 1;
                ArgumentUnderstood = TRUE;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    if (StartArg == 0 || StartArg == ArgC) {
        if (!LsofDumpHandles()) {
            return EXIT_FAILURE;
        }
    } else {

        if (DllNtDll.pNtQueryInformationFile == NULL ||
            DllKernel32.pQueryFullProcessImageNameW == NULL) {

            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("lsof: OS support not present\n"));
            return EXIT_FAILURE;
        }

        //
        //  Attempt to enable backup privilege so an administrator can access
        //  more objects successfully.
        //

        YoriLibEnableBackupPrivilege();

        LsofContext.BufferLength = 16 * 1024;
        LsofContext.Buffer = YoriLibMalloc(LsofContext.BufferLength);
        if (LsofContext.Buffer == NULL) {
            return EXIT_FAILURE;
        }

        //
        //  If no file name is specified, use stdin; otherwise open
        //  the file and use that
        //

        MatchFlags = YORILIB_FILEENUM_RETURN_FILES | YORILIB_FILEENUM_RETURN_DIRECTORIES;
        if (Recursive) {
            MatchFlags |= YORILIB_FILEENUM_RECURSE_BEFORE_RETURN | YORILIB_FILEENUM_RECURSE_PRESERVE_WILD;
        }
        if (BasicEnumeration) {
            MatchFlags |= YORILIB_FILEENUM_BASIC_EXPANSION;
        }

        for (i = StartArg; i < ArgC; i++) {

            LsofContext.FilesFoundThisArg = 0;
            YoriLibForEachStream(&ArgV[i], MatchFlags, 0, LsofFileFoundCallback, NULL, &LsofContext);
            if (LsofContext.FilesFoundThisArg == 0) {
                YORI_STRING FullPath;
                YoriLibInitEmptyString(&FullPath);
                if (YoriLibUserStringToSingleFilePath(&ArgV[i], TRUE, &FullPath)) {
                    LsofFileFoundCallback(&FullPath, NULL, 0, &LsofContext);
                    YoriLibFreeStringContents(&FullPath);
                }
            }
        }

        YoriLibFree(LsofContext.Buffer);
    }

    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>libsh/cmdbuf.c<|end_filename|>
/**
 * @file libsh/cmdbuf.c
 *
 * Facilities for managing buffers of executing processes
 *
 * Copyright (c) 2017-2019 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>
#include <yorish.h>

/**
 A buffer for a single data stream.  A process may have a different buffered
 data stream for stdout as well as stderr.
 */
typedef struct _YORI_LIBSH_PROCESS_BUFFER {

    /**
     The number of bytes currently allocated to this buffer.
     */
    DWORD BytesAllocated;

    /**
     The number of bytes populated with data in this buffer.
     */
    DWORD BytesPopulated;

    /**
     A handle to the buffer processing thread.
     */
    HANDLE hPumpThread;

    /**
     A lock for the data and sizes referred to in this structure.
     */
    HANDLE Mutex;

    /**
     A handle to a pipe which is the source of data for this buffer.
     */
    HANDLE hSource;

    /**
     A handle to a pipe which will have data pushed to it in real time
     (to support 'fg.')
     */
    HANDLE hMirror;

    /**
     The number of bytes which have been sent to hMirror.
     */
    DWORD BytesSent;

    /**
     The data buffer.
     */
    PCHAR Buffer;

} YORI_LIBSH_PROCESS_BUFFER, *PYORI_LIBSH_PROCESS_BUFFER;

/**
 A structure to record a buffered process.
 */
typedef struct _YORI_LIBSH_BUFFERED_PROCESS {

    /**
     The link into the global list of buffered processes.
     */
    YORI_LIST_ENTRY ListEntry;

    /**
     The number of references on this buffered process.
     */
    DWORD ReferenceCount;

    /**
     A handle indicating that the buffer pumping thread should terminate.
     */
    HANDLE hCancelPumpEvent;

    /**
     A buffer corresponding to the output stream from the process.
     */
    YORI_LIBSH_PROCESS_BUFFER OutputBuffer;

    /**
     A buffer corresponding to the error stream from the process.
     */
    YORI_LIBSH_PROCESS_BUFFER ErrorBuffer;

} YORI_LIBSH_BUFFERED_PROCESS, *PYORI_LIBSH_BUFFERED_PROCESS;

/**
 The global list of active buffered processes.
 */
YORI_LIST_ENTRY BufferedProcessList;

/**
 Acquire a Win32 mutex, because for some unknowable reason this isn't a
 Win32 function.

 @param Mutex The mutex to acquire.
 */
_Acquires_lock_(Mutex)
VOID
AcquireMutex(
    __in HANDLE Mutex
    )
{

    //
    //  Whether the mutex is acquired is determined by the return value and
    //  the acceptable return values are defined by the parameters.  Analyze
    //  might think we're doing something like closing the handle while using
    //  it as a synchronization primitive, but if that happened, we have
    //  bigger problems.
    //

#if defined(_MSC_VER) && (_MSC_VER >= 1700)
#pragma warning(suppress: 26166) 
#endif
    WaitForSingleObject(Mutex, INFINITE);
}

/**
 Free structures associated with a single input stream.

 @param ThisBuffer Pointer to the single stream's buffers to deallocate.
 */
VOID
YoriLibShFreeProcessBuffer(
    __in PYORI_LIBSH_PROCESS_BUFFER ThisBuffer
    )
{
    if (ThisBuffer->Buffer != NULL) {
        YoriLibFree(ThisBuffer->Buffer);
    }
    if (ThisBuffer->hMirror != NULL) {
        CloseHandle(ThisBuffer->hMirror);
    }
    if (ThisBuffer->hPumpThread != NULL) {
        CloseHandle(ThisBuffer->hPumpThread);
    }
    if (ThisBuffer->Mutex != NULL) {
        CloseHandle(ThisBuffer->Mutex);
    }
}

/**
 Free a set of process buffers.  By this point the buffers are expected to
 have no further use and no synchronization is performed.

 @param ThisBuffer The set of process buffers to free.
 */
VOID
YoriLibShFreeProcessBuffers(
    __in PYORI_LIBSH_BUFFERED_PROCESS ThisBuffer
    )
{
    YoriLibShFreeProcessBuffer(&ThisBuffer->OutputBuffer);
    YoriLibShFreeProcessBuffer(&ThisBuffer->ErrorBuffer);
    if (ThisBuffer->hCancelPumpEvent != NULL) {
        CloseHandle(ThisBuffer->hCancelPumpEvent);
    }
    YoriLibFree(ThisBuffer);
}

/**
 Code running on a dedicated thread for the duration of an outstanding process
 to populate data into its pipe.

 @param Param A pointer to the process buffer set.

 @return Thread return code, which is ignored for this thread.
 */
DWORD WINAPI
YoriLibShCmdBufferPumpToNextProcess(
    __in LPVOID Param
    )
{
    PYORI_LIBSH_PROCESS_BUFFER ThisBuffer = (PYORI_LIBSH_PROCESS_BUFFER)Param;
    DWORD BytesSent = 0;
    DWORD BytesWritten;
    DWORD BytesToWrite;

    while (TRUE) {

        BytesToWrite = 4096;
        AcquireMutex(ThisBuffer->Mutex);
        if (BytesSent + BytesToWrite > ThisBuffer->BytesPopulated) {
            BytesToWrite = ThisBuffer->BytesPopulated - BytesSent;
        }

        if (WriteFile(ThisBuffer->hSource,
                      YoriLibAddToPointer(ThisBuffer->Buffer, BytesSent),
                      BytesToWrite,
                      &BytesWritten,
                      NULL)) {

            BytesSent += BytesWritten;
        } else {
            ReleaseMutex(ThisBuffer->Mutex);
            break;
        }
        ReleaseMutex(ThisBuffer->Mutex);

        ASSERT(BytesSent <= ThisBuffer->BytesPopulated);
        if (BytesSent >= ThisBuffer->BytesPopulated) {
            break;
        }
    }

    CloseHandle(ThisBuffer->hSource);
    ThisBuffer->hSource = NULL;

    return 0;
}


/**
 Code running on a dedicated thread for the duration of an outstanding process
 to populate data into its process buffer set.

 @param Param A pointer to the process buffer set.

 @return Thread return code, which is ignored for this thread.
 */
DWORD WINAPI
YoriLibShCmdBufferPump(
    __in LPVOID Param
    )
{
    PYORI_LIBSH_PROCESS_BUFFER ThisBuffer = (PYORI_LIBSH_PROCESS_BUFFER)Param;
    DWORD BytesRead;
    HANDLE hTemp;

    while (ThisBuffer->hSource != NULL) {

        if (ReadFile(ThisBuffer->hSource,
                     YoriLibAddToPointer(ThisBuffer->Buffer, ThisBuffer->BytesPopulated),
                     ThisBuffer->BytesAllocated - ThisBuffer->BytesPopulated,
                     &BytesRead,
                     NULL)) {

            AcquireMutex(ThisBuffer->Mutex);

            if (BytesRead == 0) {
                break;
            }

            ThisBuffer->BytesPopulated += BytesRead;
            ASSERT(ThisBuffer->BytesPopulated <= ThisBuffer->BytesAllocated);
            if (ThisBuffer->BytesPopulated >= ThisBuffer->BytesAllocated) {
                DWORD NewBytesAllocated;
                PCHAR NewBuffer;

                if (ThisBuffer->BytesAllocated >= ((DWORD)-1) / 4) {
                    break;
                }

                NewBytesAllocated = ThisBuffer->BytesAllocated * 4;

                NewBuffer = YoriLibMalloc(NewBytesAllocated);
                if (NewBuffer == NULL) {
                    break;
                }

                memcpy(NewBuffer, ThisBuffer->Buffer, ThisBuffer->BytesAllocated);
                YoriLibFree(ThisBuffer->Buffer);
                ThisBuffer->Buffer = NewBuffer;
                ThisBuffer->BytesAllocated = NewBytesAllocated;
            }
        } else {
            DWORD LastError = GetLastError();

            AcquireMutex(ThisBuffer->Mutex);

            //
            //  Close the source but let the mirror drain if present.
            //

            if (LastError == ERROR_BROKEN_PIPE) {
                hTemp = ThisBuffer->hSource;
                ThisBuffer->hSource = NULL;
                CloseHandle(hTemp);
            } else {
                break;
            }
        }

        if (ThisBuffer->hMirror != NULL) {
            while (ThisBuffer->BytesSent < ThisBuffer->BytesPopulated) {
                DWORD BytesToWrite;
                DWORD BytesWritten;
                BytesToWrite = 4096;
                if (ThisBuffer->BytesSent + BytesToWrite > ThisBuffer->BytesPopulated) {
                    BytesToWrite = ThisBuffer->BytesPopulated - ThisBuffer->BytesSent;
                }

                if (WriteFile(ThisBuffer->hMirror,
                              YoriLibAddToPointer(ThisBuffer->Buffer, ThisBuffer->BytesSent),
                              BytesToWrite,
                              &BytesWritten,
                              NULL)) {

                    ThisBuffer->BytesSent += BytesWritten;
                } else {
                    hTemp = ThisBuffer->hMirror;
                    ThisBuffer->hMirror = NULL;
                    CloseHandle(hTemp);
                    ThisBuffer->BytesSent = 0;
                    break;
                }

                ASSERT(ThisBuffer->BytesSent <= ThisBuffer->BytesPopulated);
            }

        }
        ReleaseMutex(ThisBuffer->Mutex);
    }

    if (ThisBuffer->hSource != NULL) {
        hTemp = ThisBuffer->hSource;
        ThisBuffer->hSource = NULL;
        CloseHandle(hTemp);
    }

    if (ThisBuffer->hMirror != NULL) {
        hTemp = ThisBuffer->hMirror;
        ThisBuffer->hMirror = NULL;
        CloseHandle(hTemp);
    }

    ReleaseMutex(ThisBuffer->Mutex);

    return 0;
}

/**
 Allocate and initialize a buffer for a single input stream.

 @param Buffer Pointer to the buffer to allocate structures for.

 @return TRUE if the buffer is successfully initialized, FALSE if it is not.
 */
__success(return)
BOOL
YoriLibShAllocateSingleProcessBuffer(
    __out PYORI_LIBSH_PROCESS_BUFFER Buffer
    )
{
    Buffer->BytesAllocated = 1024;
    Buffer->Buffer = YoriLibMalloc(Buffer->BytesAllocated);
    if (Buffer->Buffer == NULL) {
        return FALSE;
    }

    Buffer->Mutex = CreateMutex(NULL, FALSE, NULL);
    if (Buffer->Mutex == NULL) {
        return FALSE;
    }

    return TRUE;
}

/**
 Allocate a new buffered process item.

 @param ExecContext The process whose output requires buffering.

 @return TRUE to indicate a buffer was successfully allocated, FALSE if it
         was not.
 */
__success(return)
BOOL
YoriLibShCreateNewProcessBuffer(
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext
    )
{
    PYORI_LIBSH_BUFFERED_PROCESS ThisBuffer;
    DWORD ThreadId;

    if (BufferedProcessList.Next == NULL) {
        YoriLibInitializeListHead(&BufferedProcessList);
    }

    ThisBuffer = YoriLibMalloc(sizeof(YORI_LIBSH_BUFFERED_PROCESS));
    if (ThisBuffer == NULL) {
        return FALSE;
    }

    ZeroMemory(ThisBuffer, sizeof(YORI_LIBSH_BUFFERED_PROCESS));

    //
    //  Create the buffer with two references: one for the ExecContext, and
    //  one for the pump thread, which is released in
    //  YoriLibShScanProcessBuffersForTeardown
    //

    ThisBuffer->ReferenceCount = 2;

    ThisBuffer->hCancelPumpEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
    if (ThisBuffer->hCancelPumpEvent == NULL) {
        YoriLibShFreeProcessBuffers(ThisBuffer);
        return FALSE;
    }

    //
    //  MSFIX Error handling.  If we fail during this routine, we need to
    //  ensure the caller still owns the pipes, ie., we can't just call
    //  FreeProcessBuffers at different points in this routine after some
    //  pipes are already populated.
    //

    if (ExecContext->StdOutType == StdOutTypeBuffer) {
        if (!YoriLibShAllocateSingleProcessBuffer(&ThisBuffer->OutputBuffer)) {
            YoriLibShFreeProcessBuffers(ThisBuffer);
            return FALSE;
        }

        ThisBuffer->OutputBuffer.hSource = ExecContext->StdOut.Buffer.PipeFromProcess;
        ExecContext->StdOut.Buffer.ProcessBuffers = ThisBuffer;

        ThisBuffer->OutputBuffer.hPumpThread = CreateThread(NULL, 0, YoriLibShCmdBufferPump, &ThisBuffer->OutputBuffer, 0, &ThreadId);
        if (ThisBuffer->OutputBuffer.hPumpThread == NULL) {
            YoriLibShFreeProcessBuffers(ThisBuffer);
            return FALSE;
        }

    }

    if (ExecContext->StdErrType == StdErrTypeBuffer) {
        if (!YoriLibShAllocateSingleProcessBuffer(&ThisBuffer->ErrorBuffer)) {
            YoriLibShFreeProcessBuffers(ThisBuffer);
            return FALSE;
        }

        ThisBuffer->ErrorBuffer.hSource = ExecContext->StdErr.Buffer.PipeFromProcess;
        ExecContext->StdErr.Buffer.ProcessBuffers = ThisBuffer;

        ThisBuffer->ErrorBuffer.hPumpThread = CreateThread(NULL, 0, YoriLibShCmdBufferPump, &ThisBuffer->ErrorBuffer, 0, &ThreadId);
        if (ThisBuffer->ErrorBuffer.hPumpThread == NULL) {
            YoriLibShFreeProcessBuffers(ThisBuffer);
            return FALSE;
        }
    }

    YoriLibAppendList(&BufferedProcessList, &ThisBuffer->ListEntry);

    return TRUE;
}

/**
 Append to an existing buffered process output.

 @param ExecContext The process whose output requires buffering.

 @return TRUE to indicate a buffer was successfully initialized, FALSE if it
         was not.
 */
__success(return)
BOOL
YoriLibShAppendToExistingProcessBuffer(
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext
    )
{
    PYORI_LIBSH_BUFFERED_PROCESS ThisBuffer;
    DWORD ThreadId;

    //
    //  It's not possible today to have a second process append to a previous
    //  process error stream.  In the long run this should be a child shell
    //  process, so as far as the parent is concerned there's only one process
    //  writing to buffers, and as far as the child's concerned nothing
    //  special is happening.
    //

    ASSERT(ExecContext->StdOutType == StdOutTypeBuffer);
    ASSERT(ExecContext->StdErrType != StdErrTypeBuffer);

    ThisBuffer = ExecContext->StdOut.Buffer.ProcessBuffers;
    YoriLibShWaitForProcessBufferToFinalize(ThisBuffer);
    ASSERT(WaitForSingleObject(ThisBuffer->OutputBuffer.hPumpThread, 0) == WAIT_OBJECT_0);
    CloseHandle(ThisBuffer->OutputBuffer.hPumpThread);
    ThisBuffer->OutputBuffer.hPumpThread = CreateThread(NULL, 0, YoriLibShCmdBufferPump, &ThisBuffer->OutputBuffer, 0, &ThreadId);
    if (ThisBuffer->OutputBuffer.hPumpThread == NULL) {
        return FALSE;
    }

    //
    //  Add one reference for the ExecContext.  This buffer must have
    //  already had a reference for the output pump, and it's reused
    //  for the new output pump.
    //

    YoriLibShReferenceProcessBuffer(ThisBuffer);

    return TRUE;
}

/**
 Create a background thread and pipes so that the next process will
 receive its stdin from a background thread pushing data from a buffer
 created by the previous process.  This is used on builtin commands which
 execute on the primary thread but want to output a lot of data to the
 next process.

 @param ExecContext The process whose output requires pushing to the next
        process in the chain.

 @return TRUE to indicate a pump was successfully initialized, FALSE if it
         was not.
 */
__success(return)
BOOL
YoriLibShForwardProcessBufferToNextProcess(
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext
    )
{
    PYORI_LIBSH_BUFFERED_PROCESS ThisBuffer = ExecContext->StdOut.Buffer.ProcessBuffers;
    DWORD ThreadId;
    HANDLE ReadHandle, WriteHandle;

    ASSERT(ExecContext->StdOutType == StdOutTypeBuffer);
    ASSERT(ExecContext->StdErrType != StdErrTypeBuffer);

    if (ExecContext->NextProgram != NULL &&
        ExecContext->NextProgram->StdInType == StdInTypePipe &&
        CreatePipe(&ReadHandle, &WriteHandle, NULL, 0)) {

        ExecContext->NextProgram->StdIn.Pipe.PipeFromPriorProcess = ReadHandle;

        //
        //  If we're forwarding to the next process, the previous one
        //  should be finished
        //

        if (ThisBuffer->OutputBuffer.hPumpThread != NULL) {
            if (WaitForSingleObject(ThisBuffer->OutputBuffer.hPumpThread, INFINITE) == WAIT_OBJECT_0) {
                CloseHandle(ThisBuffer->OutputBuffer.hPumpThread);
                ThisBuffer->OutputBuffer.hPumpThread = NULL;
            }
        }

        //
        //  Reverse the flow and create a thread to pump data out
        //

        ThisBuffer->OutputBuffer.hSource = WriteHandle;
        ThisBuffer->OutputBuffer.hPumpThread = CreateThread(NULL, 0, YoriLibShCmdBufferPumpToNextProcess, &ThisBuffer->OutputBuffer, 0, &ThreadId);
        if (ThisBuffer->OutputBuffer.hPumpThread == NULL) {
            return FALSE;
        }

        return TRUE;

    } else {
        return FALSE;
    }
}

/**
 Dereference an existing outstanding process buffer set.

 @param ThisBuffer The buffer set to dereference and possibly free.
 */
VOID
YoriLibShDereferenceProcessBuffer(
    __in PVOID ThisBuffer
    )
{
    PYORI_LIBSH_BUFFERED_PROCESS ThisBufferNonOpaque = (PYORI_LIBSH_BUFFERED_PROCESS)ThisBuffer;
    ThisBufferNonOpaque->ReferenceCount--;

    if (ThisBufferNonOpaque->ReferenceCount == 0) {
        YoriLibRemoveListItem(&ThisBufferNonOpaque->ListEntry);
        YoriLibShFreeProcessBuffers(ThisBufferNonOpaque);
    }
}

/**
 Add a reference to an existing process buffer set.

 @param ThisBuffer The buffer set to reference.
 */
VOID
YoriLibShReferenceProcessBuffer(
    __in PVOID ThisBuffer
    )
{
    PYORI_LIBSH_BUFFERED_PROCESS ThisBufferNonOpaque = (PYORI_LIBSH_BUFFERED_PROCESS)ThisBuffer;
    ASSERT(ThisBufferNonOpaque->ReferenceCount > 0);
    ThisBufferNonOpaque->ReferenceCount++;
}

/**
 Return contents of a process buffer.

 @param ThisBuffer Pointer to the buffer to any output from.

 @param String Pointer to a string to contain the resulting buffer.
        This buffer is expected to be unallocated on entry and is
        allocated to the correct size in this routine.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOL
YoriLibShGetProcessBuffer(
    __in PYORI_LIBSH_PROCESS_BUFFER ThisBuffer,
    __out PYORI_STRING String
    )
{
    DWORD LengthNeeded;

    if (ThisBuffer->Buffer == NULL) {
        return FALSE;
    }

    AcquireMutex(ThisBuffer->Mutex);

    LengthNeeded = YoriLibGetMultibyteInputSizeNeeded(ThisBuffer->Buffer, ThisBuffer->BytesPopulated);
    if (LengthNeeded == 0 && ThisBuffer->BytesPopulated == 0) {
        ReleaseMutex(ThisBuffer->Mutex);
        YoriLibInitEmptyString(String);
        return TRUE;
    }

    if (!YoriLibAllocateString(String, LengthNeeded)) {
        ReleaseMutex(ThisBuffer->Mutex);
        return FALSE;
    }

    YoriLibMultibyteInput(ThisBuffer->Buffer, ThisBuffer->BytesPopulated, String->StartOfString, String->LengthAllocated);
    String->LengthInChars = LengthNeeded;
    ReleaseMutex(ThisBuffer->Mutex);

    return TRUE;
}


/**
 Return contents of a process standard output buffer.

 @param ThisBuffer Pointer to the process buffers to fetch standard output
        information from.

 @param String Pointer to a string to contain the resulting standard output
        buffer.  This buffer is expected to be unallocated on entry and is
        allocated to the correct size in this routine.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOL
YoriLibShGetProcessOutputBuffer(
    __in PVOID ThisBuffer,
    __out PYORI_STRING String
    )
{
    BOOL Result;
    PYORI_LIBSH_BUFFERED_PROCESS ThisBufferNonOpaque = (PYORI_LIBSH_BUFFERED_PROCESS)ThisBuffer;
    Result = YoriLibShGetProcessBuffer(&ThisBufferNonOpaque->OutputBuffer, String);
    return Result;
}

/**
 Return contents of a process standard error buffer.

 @param ThisBuffer Pointer to the process buffers to fetch standard error
        information from.

 @param String Pointer to a string to contain the resulting standard error
        buffer.  This buffer is expected to be unallocated on entry and is
        allocated to the correct size in this routine.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOL
YoriLibShGetProcessErrorBuffer(
    __in PVOID ThisBuffer,
    __out PYORI_STRING String
    )
{
    BOOL Result;
    PYORI_LIBSH_BUFFERED_PROCESS ThisBufferNonOpaque = (PYORI_LIBSH_BUFFERED_PROCESS)ThisBuffer;
    Result = YoriLibShGetProcessBuffer(&ThisBufferNonOpaque->ErrorBuffer, String);
    return Result;
}

/**
 Either check whether a single input stream has completed or wait for it to
 complete.

 @param ThisBuffer Pointer to the single input stream to check.

 @param TeardownAll If TRUE, wait for the buffer to complete; if FALSE, check
        whether it has completed without waiting.

 @return TRUE to indicate the input stream has completed, FALSE if it has not.
 */
BOOL
YoriLibShTeardownSingleProcessBuffer(
    __in PYORI_LIBSH_PROCESS_BUFFER ThisBuffer,
    __in BOOL TeardownAll
    )
{
    BOOL Torndown = FALSE;
    if (TeardownAll) {

        //
        //  TerminateThread is inherently evil, and its use stems from the way
        //  pipes can't be waited on to indicate input, and are created
        //  synchronously, so the pump thread can be blocked in a read call
        //  with no cooperative way to communicate with it.  This call only
        //  works because it's invoked on process teardown by the main thread,
        //  so any waiters will die soon enough, and the pump thread NULLs
        //  objects before closing them so we err on the side of failing to
        //  close things rather than double close because the process is about
        //  to die anyway.  But it's still evil.
        //
#if defined(_MSC_VER) && (_MSC_VER >= 1500)
#pragma warning(disable: 6258)
#endif
        TerminateThread(ThisBuffer->hPumpThread, 0);
        if (WaitForSingleObject(ThisBuffer->hPumpThread, INFINITE) == WAIT_OBJECT_0) {
            CloseHandle(ThisBuffer->hPumpThread);
            ThisBuffer->hPumpThread = NULL;
            Torndown = TRUE;
        }
    } else {
        if (WaitForSingleObject(ThisBuffer->hPumpThread, 0) == WAIT_OBJECT_0) {
            CloseHandle(ThisBuffer->hPumpThread);
            ThisBuffer->hPumpThread = NULL;
            Torndown = TRUE;
        }
    }
    return Torndown;
}

/**
 If the threads processing a set of process buffers have completed, remove
 that buffer from the list and drop any reference attributed to it by virtue
 of having outstanding threads.  If this has already occurred no additional
 dereference will be performed.

 @param ProcessBuffers Pointer to the process buffers.

 @param TeardownAll If TRUE, terminate any threads that are still active.
 */
VOID
YoriLibShTeardownProcessBuffersIfCompletedInternal(
    __in PYORI_LIBSH_BUFFERED_PROCESS ProcessBuffers,
    __in BOOL TeardownAll
    )
{
    DWORD ThreadsFound;

    ThreadsFound = 0;
    if (ProcessBuffers->OutputBuffer.hPumpThread != NULL) {
        ThreadsFound++;
        YoriLibShTeardownSingleProcessBuffer(&ProcessBuffers->OutputBuffer, TeardownAll);
    }
    if (ProcessBuffers->ErrorBuffer.hPumpThread != NULL) {
        ThreadsFound++;
        YoriLibShTeardownSingleProcessBuffer(&ProcessBuffers->ErrorBuffer, TeardownAll);
    }

    //
    //  If there are no longer any active threads but there was before
    //  this function ran, the buffers are no longer referenced by active
    //  threads
    //

    if (ProcessBuffers->OutputBuffer.hPumpThread == NULL &&
        ProcessBuffers->ErrorBuffer.hPumpThread == NULL &&
        ThreadsFound > 0) {

        YoriLibShDereferenceProcessBuffer(ProcessBuffers);
    }
}

/**
 If the threads processing a set of process buffers have completed, remove
 that buffer from the list and drop any reference attributed to it by virtue
 of having outstanding threads.  If this has already occurred no additional
 dereference will be performed.

 @param ThisBuffer Pointer to the process buffers.
 */
VOID
YoriLibShTeardownProcessBuffersIfCompleted(
    __in PVOID ThisBuffer
    )
{
    PYORI_LIBSH_BUFFERED_PROCESS ProcessBuffers;

    ProcessBuffers = ThisBuffer;

    YoriLibShTeardownProcessBuffersIfCompletedInternal(ProcessBuffers, FALSE);
}

/**
 Scan the set of outstanding process buffers and delete any that have
 completed.

 @param TeardownAll TRUE if the shell is exiting and wants to tear down all
        state.  FALSE if this is a periodic check to tear down things that
        have been around a while.

 @return Always TRUE currently.
 */
__success(return)
BOOL
YoriLibShScanProcessBuffersForTeardown(
    __in BOOL TeardownAll
    )
{
    PYORI_LIBSH_BUFFERED_PROCESS ThisBuffer;
    PYORI_LIST_ENTRY ListEntry;

    if (BufferedProcessList.Next == NULL) {
        return TRUE;
    }

    ListEntry = YoriLibGetNextListEntry(&BufferedProcessList, NULL);
    while (ListEntry != NULL) {
        ThisBuffer = CONTAINING_RECORD(ListEntry, YORI_LIBSH_BUFFERED_PROCESS, ListEntry);
        ListEntry = YoriLibGetNextListEntry(&BufferedProcessList, ListEntry);
        if (ThisBuffer->hCancelPumpEvent != NULL && TeardownAll) {
            SetEvent(ThisBuffer->hCancelPumpEvent);
        }

        YoriLibShTeardownProcessBuffersIfCompletedInternal(ThisBuffer, TeardownAll);
    }

    return TRUE;
}

/**
 Wait for a buffer to have complete contents by waiting for the thread that
 is inputting contents to terminate.  Note this thread is not synchronized
 with any process termination, so ensuring complete contents requires waiting
 for the thread to drain, which may occur after any process generating output
 has terminated.

 @param ThisBuffer Pointer to the buffer to wait for completion.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOL
YoriLibShWaitForProcessBufferToFinalize(
    __in PVOID ThisBuffer
    )
{
    PYORI_LIBSH_BUFFERED_PROCESS ThisBufferNonOpaque = (PYORI_LIBSH_BUFFERED_PROCESS)ThisBuffer;
    if (ThisBufferNonOpaque->OutputBuffer.hPumpThread != NULL) {
        if (WaitForSingleObject(ThisBufferNonOpaque->OutputBuffer.hPumpThread, INFINITE) != WAIT_OBJECT_0) {
            ASSERT(FALSE);
            return FALSE;
        }
    }
    if (ThisBufferNonOpaque->ErrorBuffer.hPumpThread != NULL) {
        if (WaitForSingleObject(ThisBufferNonOpaque->ErrorBuffer.hPumpThread, INFINITE) != WAIT_OBJECT_0) {
            ASSERT(FALSE);
            return FALSE;
        }
    }
    return TRUE;
}

/**
 Take any existing output from a set of buffers and send it to a pipe handle,
 and continue sending further output into the pipe handle.

 @param ThisBuffer Pointer to the buffers to forward output from.

 @param hPipeOutput Specifies a pipe to forward job standard output into.

 @param hPipeErrors Specifies a pipe to forward job standard error into.

 @return TRUE to indicate success, FALSE to indicate error.
 */
__success(return)
BOOL
YoriLibShPipeProcessBuffers(
    __in PVOID ThisBuffer,
    __in_opt HANDLE hPipeOutput,
    __in_opt HANDLE hPipeErrors
    )
{
    BOOL HaveOutput;
    BOOL HaveErrors;
    BOOL Collision;
    PYORI_LIBSH_BUFFERED_PROCESS ThisBufferNonOpaque = (PYORI_LIBSH_BUFFERED_PROCESS)ThisBuffer;

    HaveOutput = FALSE;
    HaveErrors = FALSE;

    //
    //  Check if data exists for the streams that redirection is requested
    //  for.
    //

    if (hPipeOutput != NULL) {
        if (ThisBufferNonOpaque->OutputBuffer.Buffer != NULL) {
            HaveOutput = TRUE;
        } else {
            return FALSE;
        }
    }

    if (hPipeErrors != NULL) {
        if (ThisBufferNonOpaque->ErrorBuffer.Buffer != NULL) {
            HaveErrors = TRUE;
        } else {
            return FALSE;
        }
    }

    if (HaveOutput) {
        AcquireMutex(ThisBufferNonOpaque->OutputBuffer.Mutex);
    }
    if (HaveErrors) {
        AcquireMutex(ThisBufferNonOpaque->ErrorBuffer.Mutex);
    }

    //
    //  Check if the redirection requested is already being performed.
    //

    Collision = FALSE;

    if (HaveOutput) {
        if (ThisBufferNonOpaque->OutputBuffer.hMirror != NULL ||
            ThisBufferNonOpaque->OutputBuffer.hSource == NULL) {
            Collision = TRUE;
        }
    }

    if (HaveErrors) {
        if (ThisBufferNonOpaque->ErrorBuffer.hMirror != NULL ||
            ThisBufferNonOpaque->ErrorBuffer.hSource == NULL) {
            Collision = TRUE;
        }
    }

    if (Collision) {
        if (HaveOutput) {
            ReleaseMutex(ThisBufferNonOpaque->OutputBuffer.Mutex);
        }
        if (HaveErrors) {
            ReleaseMutex(ThisBufferNonOpaque->ErrorBuffer.Mutex);
        }
        return FALSE;
    }

    //
    //  While locks are acquired, update the mirror handle.
    //

    if (HaveOutput) {
        ThisBufferNonOpaque->OutputBuffer.hMirror = hPipeOutput;
        ASSERT(ThisBufferNonOpaque->OutputBuffer.BytesSent == 0);
    }

    if (HaveErrors) {
        ThisBufferNonOpaque->ErrorBuffer.hMirror = hPipeErrors;
        ASSERT(ThisBufferNonOpaque->ErrorBuffer.BytesSent == 0);
    }

    if (HaveOutput) {
        ReleaseMutex(ThisBufferNonOpaque->OutputBuffer.Mutex);
    }
    if (HaveErrors) {
        ReleaseMutex(ThisBufferNonOpaque->ErrorBuffer.Mutex);
    }

    return TRUE;
}

// vim:sw=4:ts=4:et:


<|start_filename|>for/Makefile<|end_filename|>

MODULES=\
	for.com

BINARIES=yfor.exe

!INCLUDE "..\config\common.mk"

!IF $(PDB)==1
LINKPDB=/Pdb:yfor.pdb
!ENDIF

BIN_OBJS=\
	for.obj        \

MOD_OBJS=\
	mod_for.obj    \

compile: $(BIN_OBJS) builtins.lib

yfor.exe: $(BIN_OBJS) $(YORILIB) $(YORICRT) $(YORISH) $(YORIVER)
	@echo $@
	@$(LINK) $(LDFLAGS) -entry:$(YENTRY) $(BIN_OBJS) $(LIBS) $(CRTLIB) $(YORILIB) $(YORISH) $(YORIVER) -version:$(YORI_VER_MAJOR).$(YORI_VER_MINOR) $(LINKPDB) -out:$@

mod_for.obj: for.c
	@echo $@
	@cl -c -DYORI_BUILTIN=1 $(CFLAGS) -Fo$@ for.c

builtins.lib: $(MOD_OBJS)
	@echo $@
	@$(LIB32) $(LIBFLAGS) $(MOD_OBJS) -out:$@

for.com: $(MOD_OBJS) $(YORIVER)
	@echo $@
	@$(LINK) $(LDFLAGS) $(MOD_OBJS) -dll $(LIBS) $(CRTLIB) $(YORILIB) $(YORISH) $(YORIVER) -version:$(YORI_VER_MAJOR).$(YORI_VER_MINOR) -def:for.def -out:$@


<|start_filename|>libwin/color.c<|end_filename|>
/**
 * @file libwin/color.c
 *
 * Yori window default color schemes
 *
 * Copyright (c) 2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "yoripch.h"
#include "yorilib.h"
#include "yoriwin.h"
#include "winpriv.h"


/**
 A table of colors to use by default for controls displaying on a traditional
 16-color VGA display with foreground and background support.
 */
CONST UCHAR YoriWinVgaColors[] = {
    BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE, // YoriWinColorWindowDefault
    BACKGROUND_BLUE | BACKGROUND_INTENSITY | FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY, // YoriWinTitleBarDefault
    BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE, // YoriWinColorMenuDefault
    FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE, // YoriWinColorMenuSelected
    BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE | FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY, // YoriWinColorMenuAccelerator
    FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY, // YoriWinColorMenuSelectedAccelerator
    BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE, // YoriWinColorMultilineCaption
    FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE, // YoriWinColorEditSelectedText
    BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE | FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY, // YoriWinColorAcceleratorDefault
    FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE, // YoriWinColorListActive
    FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE, // YoriWinColorControlSelected
};

/**
 A table of colors to use by default for controls displaying on an Nano
 Server, which has 16 foreground colors but no background colors.
 */
CONST UCHAR YoriWinNanoColors[] = {
    FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE, // YoriWinColorWindowDefault
    FOREGROUND_GREEN | FOREGROUND_INTENSITY, // YoriWinTitleBarDefault
    FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE, // YoriWinColorMenuDefault
    FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY, // YoriWinColorMenuSelected
    FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY, // YoriWinColorMenuAccelerator
    FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY, // YoriWinColorMenuSelectedAccelerator
    FOREGROUND_GREEN | FOREGROUND_INTENSITY, // YoriWinColorMultilineCaption
    FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY, // YoriWinColorEditSelectedText
    FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY, // YoriWinColorAcceleratorDefault
    FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY, // YoriWinColorListActive
    FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY, // YoriWinColorControlSelected
};


/**
 Return the default color table to use on this system.
 */
YORI_WIN_COLOR_TABLE_HANDLE
YoriWinGetDefaultColorTable(VOID)
{
    if (!YoriLibDoesSystemSupportBackgroundColors()) {
        return (YORI_WIN_COLOR_TABLE_HANDLE)YoriWinNanoColors;
    }
    return (YORI_WIN_COLOR_TABLE_HANDLE)YoriWinVgaColors;
}

/**
 Lookup a specified color value within the supplied color table.

 @param ColorTableHandle Handle to a color table.

 @param ColorId An identifier of a system color.

 @return The attribute value to use.
 */
UCHAR
YoriWinDefaultColorLookup(
    __in YORI_WIN_COLOR_TABLE_HANDLE ColorTableHandle,
    __in YORI_WIN_COLOR_ID ColorId
    )
{
    PUCHAR ColorTable;

    ColorTable = (PUCHAR)ColorTableHandle;
    ASSERT(ColorId < YoriWinColorBeyondMax);
#if defined(_MSC_VER) && (_MSC_VER >= 1700)
#pragma warning(suppress: 33010) // Unchecked lower bound for enum
#endif
    return ColorTable[ColorId];
}


// vim:sw=4:ts=4:et:


<|start_filename|>lib/strmenum.c<|end_filename|>
/**
 * @file lib/strmenum.c
 *
 * Yori file stream enumeration routines
 *
 * Copyright (c) 2019 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "yoripch.h"
#include "yorilib.h"

/**
 Context information passed between the file enumerator and each file found.
 */
typedef struct _YORI_LIB_STREAM_ENUM_CONTEXT {

    /**
     The callback to invoke for each matching stream.
     */
    PYORILIB_FILE_ENUM_FN UserCallback;

    /**
     The callback to invoke on enumerate error.
     */
    PYORILIB_FILE_ENUM_ERROR_FN UserErrorCallback;

    /**
     The context to pass to the stream match callbacks.
     */
    PVOID UserContext;

    /**
     The stream name from the initial enumeration criteria.
     */
    YORI_STRING TrailingStreamName;

    /**
     A temporary buffer used for each stream found out of a single
     allocation.
     */
    YORI_STRING FullPathWithStream;

    /**
     TRUE if TrailingStreamName contains wildcards which implies that stream
     enumeration is needed to check for matches.  If FALSE, the stream is
     fully specified so each file only needs to check for its existence.
     */
    BOOLEAN StreamNameHasWild;
} YORI_LIB_STREAM_ENUM_CONTEXT, *PYORI_LIB_STREAM_ENUM_CONTEXT;

/**
 A callback that is invoked when a file is found that matches the search
 criteria specified for the file component.

 @param FilePath Pointer to the file path that was found.

 @param FileInfo Information about the file.

 @param Depth Recursion depth.

 @param Context Pointer to the stream enumeration context structure indicating
        the streams to look for and callback to invoke on any match.

 @return TRUE to continute enumerating, FALSE to abort.
 */
BOOL
YoriLibStreamEnumFileFoundCallback(
    __in PYORI_STRING FilePath,
    __in PWIN32_FIND_DATA FileInfo,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    PYORI_LIB_STREAM_ENUM_CONTEXT StreamContext = (PYORI_LIB_STREAM_ENUM_CONTEXT)Context;
    WIN32_FIND_DATA BogusFileInfo;
    DWORD LengthNeeded;
    BOOL Result;

    //
    //  Make sure we have enough to hold the file name with the stream name.
    //  We must have two extra chars, one for a colon and one for a NULL.
    //  When doing stream enumerate, it's convenient to ensure we have enough
    //  for any conceivable stream; otherwise, we know the stream name length.
    //  Try to allocate a little more to avoid repeat allocations on later
    //  file matches.
    //

    if (StreamContext->StreamNameHasWild) {
        LengthNeeded = FilePath->LengthInChars + 1 + YORI_LIB_MAX_STREAM_NAME + 1;
    } else {
        LengthNeeded = FilePath->LengthInChars + 1 + StreamContext->TrailingStreamName.LengthInChars + 1;
    }
    if (StreamContext->FullPathWithStream.LengthAllocated < LengthNeeded) {
        YoriLibFreeStringContents(&StreamContext->FullPathWithStream);
        if (!YoriLibAllocateString(&StreamContext->FullPathWithStream, LengthNeeded + 200)) {
            return FALSE;
        }
    }

    if (!StreamContext->StreamNameHasWild) {

        //
        //  Build the full path to the stream
        //

        StreamContext->FullPathWithStream.LengthInChars = YoriLibSPrintf(StreamContext->FullPathWithStream.StartOfString, _T("%y:%y"), FilePath, &StreamContext->TrailingStreamName);

        //
        //  Check if it exists, and if so, call the user's callback
        //

        if (GetFileAttributes(StreamContext->FullPathWithStream.StartOfString) != 0xFFFFFFFF) {
            //
            //  Assume file state is stream state
            //

            memcpy(&BogusFileInfo, FileInfo, FIELD_OFFSET(WIN32_FIND_DATA, cFileName));
            BogusFileInfo.cAlternateFileName[0] = '\0';

            //
            //  Populate stream name
            //

            YoriLibSPrintfS(BogusFileInfo.cFileName, sizeof(BogusFileInfo.cFileName)/sizeof(BogusFileInfo.cFileName[0]), _T("%s:%y"), FileInfo->cFileName, &StreamContext->TrailingStreamName);

            //
            //  Update stream information
            //

            YoriLibUpdateFindDataFromFileInformation(&BogusFileInfo, StreamContext->FullPathWithStream.StartOfString, FALSE);


            Result = StreamContext->UserCallback(&StreamContext->FullPathWithStream,
                                                 &BogusFileInfo,
                                                 Depth,
                                                 StreamContext->UserContext);

            return Result;
        }
    } else {
        HANDLE hFind;
        WIN32_FIND_STREAM_DATA FindStreamData;
        YORI_STRING FoundStreamName;

        hFind = DllKernel32.pFindFirstStreamW(FilePath->StartOfString,
                                              0,
                                              &FindStreamData,
                                              0);
        if (hFind != INVALID_HANDLE_VALUE) {
            YoriLibInitEmptyString(&FoundStreamName);
            do {
                FoundStreamName.StartOfString = FindStreamData.cStreamName;
                FoundStreamName.LengthInChars = (DWORD)_tcslen(FindStreamData.cStreamName);

                //
                //  Truncate any trailing :$DATA attribute name
                //

                if (FoundStreamName.LengthInChars > 6 && _tcscmp(FindStreamData.cStreamName + FoundStreamName.LengthInChars - 6, L":$DATA") == 0) {
                    FoundStreamName.LengthInChars = FoundStreamName.LengthInChars - 6;
                }

                FoundStreamName.StartOfString[FoundStreamName.LengthInChars] = '\0';

                //
                //  Remove any leading colon
                //

                if (FoundStreamName.LengthInChars >= 1 && FoundStreamName.StartOfString[0] == ':') {
                    FoundStreamName.StartOfString++;
                    FoundStreamName.LengthInChars--;
                }

                //
                //  Check if it matches the specified criteria
                //

                if (YoriLibDoesFileMatchExpression(&FoundStreamName, &StreamContext->TrailingStreamName)) {

                    if (FoundStreamName.LengthInChars == 0) {
                        Result = StreamContext->UserCallback(FilePath,
                                                             FileInfo,
                                                             Depth,
                                                             StreamContext->UserContext);

                        if (Result == FALSE) {
                            FindClose(hFind);
                            return Result;
                        }
                    } else {

                        //
                        //  Generate a full path to the stream
                        //

                        StreamContext->FullPathWithStream.LengthInChars = YoriLibSPrintfS(StreamContext->FullPathWithStream.StartOfString, StreamContext->FullPathWithStream.LengthAllocated, _T("%y:%y"), FilePath, &FoundStreamName);

                        //
                        //  Assume file state is stream state
                        //

                        memcpy(&BogusFileInfo, FileInfo, FIELD_OFFSET(WIN32_FIND_DATA, cFileName));
                        BogusFileInfo.cAlternateFileName[0] = '\0';

                        //
                        //  Populate stream name
                        //

                        YoriLibSPrintfS(BogusFileInfo.cFileName, sizeof(BogusFileInfo.cFileName)/sizeof(BogusFileInfo.cFileName[0]), _T("%s:%y"), FileInfo->cFileName, &FoundStreamName);

                        //
                        //  Update stream information
                        //

                        YoriLibUpdateFindDataFromFileInformation(&BogusFileInfo, StreamContext->FullPathWithStream.StartOfString, FALSE);

                        Result = StreamContext->UserCallback(&StreamContext->FullPathWithStream,
                                                             &BogusFileInfo,
                                                             Depth,
                                                             StreamContext->UserContext);

                        if (Result == FALSE) {
                            FindClose(hFind);
                            return Result;
                        }
                    }
                }
            } while(DllKernel32.pFindNextStreamW(hFind, &FindStreamData));

            FindClose(hFind);
        }
    }

    return TRUE;
}

/**
 A callback that is invoked when a directory cannot be successfully enumerated.

 @param FilePath Pointer to the file path that could not be enumerated.

 @param ErrorCode The Win32 error code describing the failure.

 @param Depth Recursion depth, ignored in this application.

 @param Context Pointer to the stream enumeration context, indicating which
        callback and context to invoke on error.

 @return TRUE to continute enumerating, FALSE to abort.
 */
BOOL
YoriLibStreamEnumErrorCallback(
    __in PYORI_STRING FilePath,
    __in DWORD ErrorCode,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    PYORI_LIB_STREAM_ENUM_CONTEXT StreamContext = (PYORI_LIB_STREAM_ENUM_CONTEXT)Context;

    if (StreamContext->UserErrorCallback != NULL) {
        return StreamContext->UserErrorCallback(FilePath, ErrorCode, Depth, StreamContext->UserContext);
    }

    return TRUE;
}


/**
 Enumerate the set of possible streams matching a user specified pattern.
 This function is responsible for initiating enumeration of files, and then
 applying a filter for matching streams, and invoking a caller specified
 callback for matching streams.

 @param FileSpec The user provided file specification to enumerate matches on.

 @param MatchFlags Specifies the behavior of the match, including whether
        it should be applied recursively and the recursing behavior.

 @param Depth Indicates the current recursion depth.  If this function is
        reentered, this value is incremented.

 @param Callback The callback to invoke on each match.

 @param ErrorCallback Optionally points to a function to invoke if a
        directory cannot be enumerated.  If NULL, the caller does not care
        about failures and wants to silently continue.

 @param Context Caller provided context to pass to the callback.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriLibForEachStream(
    __in PYORI_STRING FileSpec,
    __in DWORD MatchFlags,
    __in DWORD Depth,
    __in PYORILIB_FILE_ENUM_FN Callback,
    __in_opt PYORILIB_FILE_ENUM_ERROR_FN ErrorCallback,
    __in PVOID Context
    )
{
    YORI_STRING FilePart;
    YORI_STRING FileSpecNoStream;
    DWORD Index;
    BOOL Result;
    YORI_LIB_STREAM_ENUM_CONTEXT StreamContext;

    //
    //  Look backwards through the FileSpec to see if there's a seperator.
    //  Any stream specification is only valid after that point.
    //

    YoriLibInitEmptyString(&FilePart);
    for (Index = FileSpec->LengthInChars; Index > 0; Index--) {
        if (YoriLibIsSep(FileSpec->StartOfString[Index - 1])) {
            FilePart.StartOfString = &FileSpec->StartOfString[Index];
            FilePart.LengthInChars = FileSpec->LengthInChars - Index;
            break;
        }
    }

    if (FilePart.StartOfString == NULL) {
        FilePart.StartOfString = FileSpec->StartOfString;
        FilePart.LengthInChars = FileSpec->LengthInChars;
    }

    //
    //  Now look through the file component for a stream seperator.
    //

    StreamContext.StreamNameHasWild = FALSE;
    YoriLibInitEmptyString(&StreamContext.TrailingStreamName);
    for (Index = FilePart.LengthInChars; Index > 0; Index--) {
        if (FilePart.StartOfString[Index - 1] == ':') {

            // 
            //  If the colon is the second character, and the first char is
            //  A through Z, assume this is a drive specification, not a
            //  stream specification.  "C:Foo" is ambiguous, but Windows will
            //  treat it as a drive, so we do too.
            //

            if (Index == 2) {
                TCHAR UpcasedFirstChar;
                UpcasedFirstChar = YoriLibUpcaseChar(FilePart.StartOfString[0]);
                if (UpcasedFirstChar >= 'A' && UpcasedFirstChar <= 'Z') {
                    break;
                }
            }
            StreamContext.TrailingStreamName.StartOfString = &FilePart.StartOfString[Index];
            StreamContext.TrailingStreamName.LengthInChars = FilePart.LengthInChars - Index;
            break;
        } else if (FilePart.StartOfString[Index - 1] == '*' || FilePart.StartOfString[Index - 1] == '?') {
            StreamContext.StreamNameHasWild = TRUE;
        }
    }

    //
    //  If there is no stream specification, we only need to consider any
    //  file matches, so pass this to the file enumerator.
    //

    if (StreamContext.TrailingStreamName.LengthInChars == 0) {
        return YoriLibForEachFile(FileSpec, MatchFlags, Depth, Callback, ErrorCallback, Context);
    }

    YoriLibInitEmptyString(&FileSpecNoStream);
    FileSpecNoStream.StartOfString = FileSpec->StartOfString;
    FileSpecNoStream.LengthInChars = FileSpec->LengthInChars - StreamContext.TrailingStreamName.LengthInChars - 1;

    FileSpecNoStream.MemoryToFree = YoriLibCStringFromYoriString(&FileSpecNoStream);
    if (FileSpecNoStream.MemoryToFree == NULL) {
        return FALSE;
    }
    FileSpecNoStream.LengthAllocated = FileSpecNoStream.LengthInChars + 1;
    FileSpecNoStream.StartOfString = FileSpecNoStream.MemoryToFree;

    StreamContext.UserCallback = Callback;
    StreamContext.UserErrorCallback = ErrorCallback;
    StreamContext.UserContext = Context;
    YoriLibInitEmptyString(&StreamContext.FullPathWithStream);

    //
    //  If the system doesn't support it, don't try to enumerate streams.
    //  This limits the allowable expressions but fully specified stream
    //  names will still work.
    //

    if (DllKernel32.pFindFirstStreamW == NULL) {
        StreamContext.StreamNameHasWild = FALSE;
    }

    Result = YoriLibForEachFile(&FileSpecNoStream,
                                MatchFlags,
                                Depth,
                                YoriLibStreamEnumFileFoundCallback,
                                YoriLibStreamEnumErrorCallback,
                                &StreamContext);

    YoriLibFreeStringContents(&StreamContext.FullPathWithStream);
    YoriLibFreeStringContents(&FileSpecNoStream);
    return Result;
}

// vim:sw=4:ts=4:et:


<|start_filename|>for/for.c<|end_filename|>
/**
 * @file for/for.c
 *
 * Yori shell enumerate and operate on strings or files
 *
 * Copyright (c) 2017-2019 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>
#include <yorish.h>
#ifdef YORI_BUILTIN
#include <yoricall.h>
#endif

/**
 Help text to display to the user.
 */
const
CHAR strForHelpText[] =
        "Enumerates through a list of strings or files.\n"
        "\n"
        "FOR [-license] [-b] [-c] [-d] [-i <criteria>] [-l] [-p n] [-r]\n"
        "    <var> in (<list>) do <cmd>\n"
        "\n"
        "   -b             Use basic search criteria for files only\n"
        "   -c             Use cmd as a subshell rather than Yori\n"
        "   -d             Match directories rather than files\n"
        "   -i <criteria>  Only treat match files if they meet criteria, see below\n"
        "   -l             Use (start,step,end) notation for the list\n"
        "   -p <n>         Execute with <n> concurrent processes\n"
        "   -r             Look for matches in subdirectories under the current directory\n"
        "\n"
        " The -i option will match files only if they meet criteria.  This is a\n"
        " semicolon delimited list of entries matching the following form:\n"
        "\n"
        "   [file attribute][operator][criteria]\n";

/**
 Display usage text to the user.
 */
BOOL
ForHelp(VOID)
{

    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("For %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strForHelpText);

    //
    //  Display supported options and operators
    //

    YoriLibFileFiltHelp();
    return TRUE;
}

/**
 State about the currently running processes as well as information required
 to launch any new processes from this program.
 */
typedef struct _FOR_EXEC_CONTEXT {

    /**
     If TRUE, use CMD as a subshell.  If FALSE, use Yori.
     */
    BOOL InvokeCmd;

    /**
     The string that might be found in ArgV which should be changed to contain
     the value of any match.
     */
    PYORI_STRING SubstituteVariable;

    /**
     The number of elements in ArgV.
     */
    DWORD ArgC;

    /**
     The template form of an argv style argument array, before any
     substitution has taken place.
     */
    PYORI_STRING ArgV;

    /**
     The number of processes that this program would like to have concurrently
     running.
     */
    DWORD TargetConcurrentCount;

    /**
     The number of processes that are currently running as a result of this
     program.
     */
    DWORD CurrentConcurrentCount;

    /**
     An array of handles with CurrentConcurrentCount number of valid
     elements.  These correspond to processes that are currently running.
     */
    PHANDLE HandleArray;

    /**
     A list of criteria to filter matches against.
     */
    YORI_LIB_FILE_FILTER Filter;

} FOR_EXEC_CONTEXT, *PFOR_EXEC_CONTEXT;

/**
 Wait for any single process to complete.

 @param ExecContext Pointer to the for exec context containing information
        about currently running processes.
 */
VOID
ForWaitForProcessToComplete(
    __in PFOR_EXEC_CONTEXT ExecContext
    )
{
    DWORD Result;
    DWORD Index;
    DWORD Count;

    Result = WaitForMultipleObjects(ExecContext->CurrentConcurrentCount, ExecContext->HandleArray, FALSE, INFINITE);

    ASSERT(Result < (WAIT_OBJECT_0 + ExecContext->CurrentConcurrentCount));

    Index = Result - WAIT_OBJECT_0;

    ASSERT(Index < ExecContext->CurrentConcurrentCount);

    CloseHandle(ExecContext->HandleArray[Index]);

    for (Count = Index; Count < ExecContext->CurrentConcurrentCount - 1; Count++) {

#if defined(_MSC_VER) && (_MSC_VER >= 1700)
        //
        //  The "correct" annotation here would describe that HandleArray is
        //  of TargetConcurrentCount elements, CurrentConcurrentCount of which
        //  are readable, and CurrentConcurrentCount needs to be less than or
        //  equal to TargetConcurrentCount.  This is something that I don't
        //  see a way to express as of this writing.
        //
#pragma warning(suppress: 6001)
#endif
        ExecContext->HandleArray[Count] = ExecContext->HandleArray[Count + 1];
    }

    ExecContext->CurrentConcurrentCount--;
}

/**
 Add a quote to a substring within an argument.  This is used because the
 string may contain backquotes, where any quotes need to only surround the
 text, without quoting the backquote.

 @param Arg Pointer to the string that contains the entire argument.  This
        argument can be reallocated within this routine as needed.

 @param ArgContext Pointer to the argument context, indicating whether the
        argument is quoted.

 @param Section Pointer to a substring within Arg.

 @param WhiteSpaceInSection If TRUE, the substring contains white space,
        indicating the substring requires quoting.

 @param CurrentPosition This function is called when processing an argument,
        and the caller describes its position with a string whose start is
        within the argument and whose length is the remainder of characters
        to process.  Because this function can reallocate the argument, this
        position must be updated, including to reflect any modifications
        performed by this routine that would adjust the position relative to
        the newly created argument.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOLEAN
ForQuoteSectionIfNeeded(
    __in PYORI_STRING Arg,
    __in PYORI_LIBSH_ARG_CONTEXT ArgContext,
    __in PYORI_STRING Section,
    __in BOOLEAN WhiteSpaceInSection,
    __inout PYORI_STRING CurrentPosition
    )
{
    DWORD Offset;

    //
    //  This routine will update the current position and depends upon the
    //  current position being relative to the current argument, and not
    //  having any extra memory to consider.
    //

    ASSERT(CurrentPosition->MemoryToFree == NULL);

    Offset = (DWORD)(CurrentPosition->StartOfString - Arg->StartOfString);

    if (!ArgContext->Quoted && WhiteSpaceInSection && Section->LengthInChars > 0) {

        //
        //  If there's whitespace in it, the section should have characters
        //

        ASSERT(Section->LengthInChars > 0);

        if (Section->StartOfString == Arg->StartOfString &&
            Section->LengthInChars == Arg->LengthInChars) {

            ArgContext->Quoted = TRUE;
            ArgContext->QuoteTerminated = TRUE;
        } else {
            YORI_STRING NewArg;
            YORI_STRING Prefix;
            YORI_STRING Suffix;
            DWORD ExtraOffsetChars;

            YoriLibInitEmptyString(&Prefix);
            YoriLibInitEmptyString(&Suffix);

            Prefix.StartOfString = Arg->StartOfString;
            Prefix.LengthInChars = (DWORD)(Section->StartOfString - Arg->StartOfString);

            Suffix.StartOfString = Section->StartOfString + Section->LengthInChars;
            Suffix.LengthInChars = Arg->LengthInChars - (DWORD)(Suffix.StartOfString - Arg->StartOfString);

            if (!YoriLibAllocateString(&NewArg, Arg->LengthInChars + 3)) {
                return FALSE;
            }

            ExtraOffsetChars = 0;
            if (Offset >= Prefix.LengthInChars + Section->LengthInChars) {
                ExtraOffsetChars += 1;
            }

            if (Offset >= Prefix.LengthInChars) {
                ExtraOffsetChars += 1;
            }

            NewArg.LengthInChars = YoriLibSPrintf(NewArg.StartOfString, _T("%y\"%y\"%y"), &Prefix, Section, &Suffix);
            YoriLibFreeStringContents(Arg);
            memcpy(Arg, &NewArg, sizeof(YORI_STRING));

            CurrentPosition->StartOfString = NewArg.StartOfString + Offset + ExtraOffsetChars;
            CurrentPosition->LengthInChars = NewArg.LengthInChars - Offset - ExtraOffsetChars;
        }
    }

    return TRUE;
}

/**
 Search through arguments for a character that would normally implicitly
 terminate an argument.  This includes shell operators like redirectors,
 pipes, or backquotes.  When these are encountered, split the argument at
 that point.  This means when quotes are added based on arguments with
 spaces, they will only span the text with spaces, not any operators.

 If there are characters within an argument, such as a backquote, the
 argument cannot be split without altering its meaning.  In this case, real
 quotes need to be inserted around a substring without splitting the
 argument.

 @param CmdContext Pointer to the command context, containing the number of
        arguments, argument values, and quote state of each argument.  These
        values may be reallocated or changed within this routine.  Note that
        this routine assumes that the contents of the strings are not part of
        these allocations, so any new string allocations are still pointing
        to the previous string locations.

 @return TRUE to indicate successful completion, FALSE to indicate failure.
 */
BOOLEAN
ForBreakArgumentsAsNeeded(
    __in PYORI_LIBSH_CMD_CONTEXT CmdContext
    )
{
    DWORD ArgCount;
    DWORD ArgIndex;
    DWORD ExistingArg;
    DWORD CharsToConsume;
    DWORD InitialArgOffset;
    DWORD BraceNestingLevel;
    PYORI_STRING Arg;
    YORI_STRING Char;
    YORI_STRING Section;
    BOOLEAN TerminateNextArg;
    BOOL QuoteOpen = FALSE;
    BOOLEAN LookingForFirstQuote = FALSE;
    BOOLEAN WhiteSpaceInSection = FALSE;

    YoriLibInitEmptyString(&Char);
    YoriLibInitEmptyString(&Section);
    ArgCount = CmdContext->ArgC;
    Arg = CmdContext->ArgV;
    InitialArgOffset = 0;
    BraceNestingLevel = 0;

    for (ArgIndex = 0; ArgIndex < ArgCount; ArgIndex++) {

        Char.StartOfString = Arg[ArgIndex].StartOfString;
        Char.LengthInChars = Arg[ArgIndex].LengthInChars;

        if (InitialArgOffset > 0) {
            Char.StartOfString = Char.StartOfString + InitialArgOffset;
            Char.LengthInChars = Char.LengthInChars - InitialArgOffset;
        }

        Section.StartOfString = Char.StartOfString;

        //
        //  Remove leading spaces
        //

        while (Char.LengthInChars > 0) {
            if (Char.StartOfString[0] == ' ') {
                WhiteSpaceInSection = TRUE;
                Char.StartOfString++;
                Char.LengthInChars--;
            } else {
                break;
            }
        }

        if (CmdContext->ArgContexts[ArgIndex].Quoted) {
            LookingForFirstQuote = TRUE;
            QuoteOpen = TRUE;
        } else if (Char.LengthInChars > 0 && Char.StartOfString[0] == '"') {
            LookingForFirstQuote = TRUE;
        } else {
            LookingForFirstQuote = FALSE;
        }

        //
        //  Go through the arg looking for operators that would indicate
        //  an argument break.  Note that unlike any normal case, spaces
        //  do not constitute an argument break.
        //

        while (Char.LengthInChars > 0) {

            if (YoriLibIsEscapeChar(Char.StartOfString[0])) {
                Char.StartOfString++;
                Char.LengthInChars--;
                if (Char.LengthInChars > 0) {
                    Char.StartOfString++;
                    Char.LengthInChars--;
                }
                continue;
            }

            if (Char.StartOfString[0] == '"' && QuoteOpen && LookingForFirstQuote) {
                QuoteOpen = FALSE;
                LookingForFirstQuote = FALSE;
                Char.StartOfString++;
                Char.LengthInChars--;
                continue;
            }

            if (Char.StartOfString[0] == '"') {
                QuoteOpen = !QuoteOpen;
                if (LookingForFirstQuote) {
                    Char.StartOfString++;
                    Char.LengthInChars--;
                    continue;
                }
            }

            if (!QuoteOpen) {

                //
                //  MSFIX: Current thinking goes something like, maintain a
                //  section string that starts at the beginning of the arg
                //  and is extended up to a terminating backquote, or the end
                //  of the string.  As we go, check if a whitespace is
                //  detected.  At the end of the section, if the arg doesn't
                //  already have quotes, insert them around that section.
                //
                //  This could be optimized in the common case (section ==
                //  arg) by setting the ArgContext flag.
                //
                //  Note that backquote accounting needs to happen across all
                //  arguments, although we "know" that a quoted arg doesn't
                //  apply backquote updates.
                //

                if (Char.StartOfString[0] == '$' &&
                    Char.LengthInChars >= 2 &&
                    Char.StartOfString[1] == '(') {

                    Section.LengthInChars = (DWORD)(Char.StartOfString - Section.StartOfString);
                    ForQuoteSectionIfNeeded(&CmdContext->ArgV[ArgIndex],
                                            &CmdContext->ArgContexts[ArgIndex],
                                            &Section,
                                            WhiteSpaceInSection,
                                            &Char);
                    Section.StartOfString = &Char.StartOfString[2];
                    WhiteSpaceInSection = FALSE;
                    BraceNestingLevel++;
                } else if (Char.StartOfString[0] == ')' &&
                           BraceNestingLevel > 0) {

                    Section.LengthInChars = (DWORD)(Char.StartOfString - Section.StartOfString);
                    ForQuoteSectionIfNeeded(&CmdContext->ArgV[ArgIndex],
                                            &CmdContext->ArgContexts[ArgIndex],
                                            &Section,
                                            WhiteSpaceInSection,
                                            &Char);
                    Section.StartOfString = &Char.StartOfString[1];
                    WhiteSpaceInSection = FALSE;
                    BraceNestingLevel--;
                } else if (Char.StartOfString[0] == '`') {
                    Section.LengthInChars = (DWORD)(Char.StartOfString - Section.StartOfString);
                    ForQuoteSectionIfNeeded(&CmdContext->ArgV[ArgIndex],
                                            &CmdContext->ArgContexts[ArgIndex],
                                            &Section,
                                            WhiteSpaceInSection,
                                            &Char);
                    Section.StartOfString = &Char.StartOfString[1];
                    WhiteSpaceInSection = FALSE;
                } else if (Char.StartOfString[0] == ' ') {
                    WhiteSpaceInSection = TRUE;
                }

                if (!CmdContext->ArgContexts[ArgIndex].Quoted &&
                    YoriLibShIsArgumentSeperator(&Char, &CharsToConsume, &TerminateNextArg)) {

                    DWORD NewArgCount;
                    YORI_LIBSH_CMD_CONTEXT NewCmd;

                    Section.LengthInChars = (DWORD)(Char.StartOfString - Section.StartOfString);
                    ForQuoteSectionIfNeeded(&CmdContext->ArgV[ArgIndex],
                                            &CmdContext->ArgContexts[ArgIndex],
                                            &Section,
                                            WhiteSpaceInSection,
                                            &Char);
                    WhiteSpaceInSection = FALSE;

                    //
                    //  If the next arg is terminated but is already
                    //  terminating, don't do it twice.
                    //

                    if (TerminateNextArg && CharsToConsume == Char.LengthInChars) {
                        TerminateNextArg = FALSE;
                    }

                    //
                    //  At this point we need to reallocate the array to add
                    //  either one or two new args.  The current arg ends at
                    //  the current offset.  The next arg starts at the
                    //  current offset.  If TerminateNextArg is TRUE, the
                    //  next arg is CharsToConsume length and the next next
                    //  arg starts after that.  We should advance the
                    //  arg index to the final arg generated here, ensure
                    //  Char.LengthInChars is zero, and continue.
                    //

                    if (TerminateNextArg) {
                        NewArgCount = ArgCount + 2;
                    } else {
                        NewArgCount = ArgCount + 1;
                    }

                    if (!YoriLibShAllocateArgCount(&NewCmd, NewArgCount, 0, NULL)) {
                        return FALSE;
                    }

                    for (ExistingArg = 0; ExistingArg <= ArgIndex; ExistingArg++) {
                        YoriLibShCopyArg(CmdContext, ExistingArg, &NewCmd, ExistingArg);
                    }

                    NewCmd.ArgV[ArgIndex].LengthInChars = (DWORD)(Char.StartOfString - Arg[ArgIndex].StartOfString);
                    YoriLibShCheckIfArgNeedsQuotes(&NewCmd, ArgIndex);
                    YoriLibInitEmptyString(&NewCmd.ArgV[ArgIndex + 1]);
                    NewCmd.ArgV[ArgIndex + 1].StartOfString = Char.StartOfString;

                    if (TerminateNextArg) {
                        NewCmd.ArgV[ArgIndex + 1].LengthInChars = CharsToConsume;
                        YoriLibShCheckIfArgNeedsQuotes(&NewCmd, ArgIndex + 1);
                        YoriLibInitEmptyString(&NewCmd.ArgV[ArgIndex + 2]);
                        NewCmd.ArgV[ArgIndex + 2].StartOfString = &Char.StartOfString[CharsToConsume];
                        NewCmd.ArgV[ArgIndex + 2].LengthInChars = Char.LengthInChars - CharsToConsume;
                        YoriLibShCheckIfArgNeedsQuotes(&NewCmd, ArgIndex + 2);
                         for (ExistingArg = ArgIndex + 1; ExistingArg < ArgCount; ExistingArg++) {
                             YoriLibShCopyArg(CmdContext, ExistingArg, &NewCmd, ExistingArg + 2);
                         }

                        ArgIndex++;

                    } else {
                        NewCmd.ArgV[ArgIndex + 1].LengthInChars = Char.LengthInChars;
                        YoriLibShCheckIfArgNeedsQuotes(&NewCmd, ArgIndex + 1);
                        for (ExistingArg = ArgIndex + 1; ExistingArg < ArgCount; ExistingArg++) {
                            YoriLibShCopyArg(CmdContext, ExistingArg, &NewCmd, ExistingArg + 1);
                        }
                        InitialArgOffset = CharsToConsume;
                    }

                    //
                    //  MSFIX: Need to update Section and Char to refer to
                    //  their respective locations in the new argument.  Note
                    //  that the argument index may have changed, and the
                    //  argument length may have changed.
                    //
                    //  The nightmare case is something like:
                    //  %i>>foo
                    //
                    //  Where we want:
                    //  "My File">>foo
                    //
                    //  Meaning that the section is being terminated by the
                    //  argument seperator but the previous section needs to
                    //  be resolved.
                    //

                    WhiteSpaceInSection = FALSE;

                    YoriLibShFreeCmdContext(CmdContext);
                    CmdContext->ArgC = NewArgCount;
                    CmdContext->ArgV = NewCmd.ArgV;
                    CmdContext->ArgContexts = NewCmd.ArgContexts;
                    CmdContext->MemoryToFree = NewCmd.MemoryToFree;

                    ArgCount = NewArgCount;
                    Arg = NewCmd.ArgV;

                    break;
                }
            }

            Char.StartOfString++;
            Char.LengthInChars--;
        }

        if (WhiteSpaceInSection) {
            Section.LengthInChars = (DWORD)(Char.StartOfString - Section.StartOfString);
            ForQuoteSectionIfNeeded(&CmdContext->ArgV[ArgIndex],
                                    &CmdContext->ArgContexts[ArgIndex],
                                    &Section,
                                    WhiteSpaceInSection,
                                    &Char);
            WhiteSpaceInSection = FALSE;
        }
    }

    return TRUE;
}

/**
 Execute a new command in response to a newly matched element.

 @param Match The match that was found from the set.

 @param ExecContext The current state of child processes and information about
        the arguments for any new child process.
 */
VOID
ForExecuteCommand(
    __in PYORI_STRING Match,
    __in PFOR_EXEC_CONTEXT ExecContext
    )
{
    DWORD ArgsNeeded = ExecContext->ArgC;
    DWORD PrefixArgCount;
    DWORD FoundOffset;
    DWORD Count;
    DWORD SubstitutesFound;
    DWORD ArgLengthNeeded;
    YORI_STRING OldArg;
    YORI_STRING NewArgWritePoint;
    YORI_STRING CmdLine;
    PROCESS_INFORMATION ProcessInfo;
    STARTUPINFO StartupInfo;
    YORI_LIBSH_CMD_CONTEXT NewCmd;

    YoriLibInitEmptyString(&CmdLine);

#ifdef YORI_BUILTIN
    if (!ExecContext->InvokeCmd &&
        ExecContext->TargetConcurrentCount == 1) {
        PrefixArgCount = 0;
    } else {
        PrefixArgCount = 2;
    }
#else
    PrefixArgCount = 2;
#endif

    ArgsNeeded += PrefixArgCount;

    if (!YoriLibShAllocateArgCount(&NewCmd, ArgsNeeded, 0, NULL)) {
        return;
    }

    //
    //  If a full path is specified in the environment, use it.  If not,
    //  use a file name only and let PATH evaluation find an interpreter.
    //

    if (PrefixArgCount > 0) {
        if (ExecContext->InvokeCmd) {
            ArgLengthNeeded = GetEnvironmentVariable(_T("COMSPEC"), NULL, 0);
            if (ArgLengthNeeded != 0) {
                if (YoriLibAllocateString(&NewCmd.ArgV[0], ArgLengthNeeded)) {
                    NewCmd.ArgV[0].LengthInChars = GetEnvironmentVariable(_T("COMSPEC"), NewCmd.ArgV[0].StartOfString, NewCmd.ArgV[0].LengthAllocated);
                    if (NewCmd.ArgV[0].LengthInChars == 0) {
                        YoriLibFreeStringContents(&NewCmd.ArgV[0]);
                    }
                }
            }
            if (NewCmd.ArgV[0].LengthInChars == 0) {
                YoriLibConstantString(&NewCmd.ArgV[0], _T("cmd.exe"));
            }
        } else {
            ArgLengthNeeded = GetEnvironmentVariable(_T("YORISPEC"), NULL, 0);
            if (ArgLengthNeeded != 0) {
                if (YoriLibAllocateString(&NewCmd.ArgV[0], ArgLengthNeeded)) {
                    NewCmd.ArgV[0].LengthInChars = GetEnvironmentVariable(_T("YORISPEC"), NewCmd.ArgV[0].StartOfString, NewCmd.ArgV[0].LengthAllocated);
                    if (NewCmd.ArgV[0].LengthInChars == 0) {
                        YoriLibFreeStringContents(&NewCmd.ArgV[0]);
                    }
                }
            }
            if (NewCmd.ArgV[0].LengthInChars == 0) {
                YoriLibConstantString(&NewCmd.ArgV[0], _T("yori.exe"));
            }
        }
        YoriLibConstantString(&NewCmd.ArgV[1], _T("/c"));
    }

    //
    //  Go through all of the arguments, count the number of variable
    //  substitutions to apply, allocate a string to contain the subtituted
    //  value, and copy the string into the new allocation applying
    //  substitutions.
    //

    for (Count = 0; Count < ExecContext->ArgC; Count++) {
        YoriLibInitEmptyString(&OldArg);
        OldArg.StartOfString = ExecContext->ArgV[Count].StartOfString;
        OldArg.LengthInChars = ExecContext->ArgV[Count].LengthInChars;
        SubstitutesFound = 0;

        //
        //  MSFIX: Ideally this would have ArgContexts for the input arguments
        //  and copy quote state of those rather than trying to infer whether
        //  a quote is needed.  It makes complete sense for a user to specify
        //  "%i" or similar (with quotes) as an input and expect them to be
        //  applied universally.  For the external version of for, this could
        //  be done by passing GetCommandLine() to YoriSh, but the builtin
        //  version requires a little more thought.
        //

        YoriLibInitEmptyString(&NewCmd.ArgV[Count + PrefixArgCount]);
        NewCmd.ArgV[Count + PrefixArgCount].StartOfString = OldArg.StartOfString;
        NewCmd.ArgV[Count + PrefixArgCount].LengthInChars = OldArg.LengthInChars;

        YoriLibShCheckIfArgNeedsQuotes(&NewCmd, Count + PrefixArgCount);

        while (YoriLibFindFirstMatchingSubstring(&OldArg, 1, ExecContext->SubstituteVariable, &FoundOffset)) {
            SubstitutesFound++;
            OldArg.StartOfString += FoundOffset + 1;
            OldArg.LengthInChars -= FoundOffset + 1;
        }

        ArgLengthNeeded = ExecContext->ArgV[Count].LengthInChars + SubstitutesFound * Match->LengthInChars - SubstitutesFound * ExecContext->SubstituteVariable->LengthInChars + 1;
        if (!YoriLibAllocateString(&NewCmd.ArgV[Count + PrefixArgCount], ArgLengthNeeded)) {
            goto Cleanup;
        }

        YoriLibInitEmptyString(&NewArgWritePoint);
        NewArgWritePoint.StartOfString = NewCmd.ArgV[Count + PrefixArgCount].StartOfString;
        NewArgWritePoint.LengthAllocated = NewCmd.ArgV[Count + PrefixArgCount].LengthAllocated;

        YoriLibInitEmptyString(&OldArg);
        OldArg.StartOfString = ExecContext->ArgV[Count].StartOfString;
        OldArg.LengthInChars = ExecContext->ArgV[Count].LengthInChars;

        while (TRUE) {
            if (YoriLibFindFirstMatchingSubstring(&OldArg, 1, ExecContext->SubstituteVariable, &FoundOffset)) {
                memcpy(NewArgWritePoint.StartOfString, OldArg.StartOfString, FoundOffset * sizeof(TCHAR));
                NewArgWritePoint.StartOfString += FoundOffset;
                NewArgWritePoint.LengthAllocated -= FoundOffset;
                memcpy(NewArgWritePoint.StartOfString, Match->StartOfString, Match->LengthInChars * sizeof(TCHAR));
                NewArgWritePoint.StartOfString += Match->LengthInChars;
                NewArgWritePoint.LengthAllocated -= Match->LengthInChars;

                OldArg.StartOfString += FoundOffset + ExecContext->SubstituteVariable->LengthInChars;
                OldArg.LengthInChars -= FoundOffset + ExecContext->SubstituteVariable->LengthInChars;
            } else {
                memcpy(NewArgWritePoint.StartOfString, OldArg.StartOfString, OldArg.LengthInChars * sizeof(TCHAR));
                NewArgWritePoint.StartOfString += OldArg.LengthInChars;
                NewArgWritePoint.LengthAllocated -= OldArg.LengthInChars;
                NewArgWritePoint.StartOfString[0] = '\0';

                NewCmd.ArgV[Count + PrefixArgCount].LengthInChars = (DWORD)(NewArgWritePoint.StartOfString - NewCmd.ArgV[Count + PrefixArgCount].StartOfString);
                ASSERT(NewCmd.ArgV[Count + PrefixArgCount].LengthInChars < NewCmd.ArgV[Count + PrefixArgCount].LengthAllocated);
                ASSERT(YoriLibIsStringNullTerminated(&NewCmd.ArgV[Count + PrefixArgCount]));
                break;
            }
        }
    }

    //
    //  Split arguments where meaning has changed. This is because when
    //  converting the arguments into a combined string, quotes will be
    //  inserted around arguments containing spaces.  However, the location
    //  of the quotes matters:
    //
    //  My Text>foo
    //  should be
    //  "My Text">foo
    //
    //  `My Text`
    //  should be
    //  `"My Text"`
    //
    //  etc.
    //

    ForBreakArgumentsAsNeeded(&NewCmd);

    if (!YoriLibShBuildCmdlineFromCmdContext(&NewCmd, &CmdLine, FALSE, NULL, NULL)) {
        goto Cleanup;
    }

#ifdef YORI_BUILTIN
    if (PrefixArgCount == 0) {
        YoriCallExecuteExpression(&CmdLine);
        goto Cleanup;
    }
#endif

    memset(&StartupInfo, 0, sizeof(StartupInfo));
    StartupInfo.cb = sizeof(StartupInfo);

    if (!CreateProcess(NULL, CmdLine.StartOfString, NULL, NULL, TRUE, CREATE_DEFAULT_ERROR_MODE, NULL, NULL, &StartupInfo, &ProcessInfo)) {
        DWORD LastError = GetLastError();
        LPTSTR ErrText = YoriLibGetWinErrorText(LastError);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("for: execution failed: %s"), ErrText);
        YoriLibFreeWinErrorText(ErrText);
        goto Cleanup;
    }

    CloseHandle(ProcessInfo.hThread);

    ExecContext->HandleArray[ExecContext->CurrentConcurrentCount] = ProcessInfo.hProcess;
    ExecContext->CurrentConcurrentCount++;

    if (ExecContext->CurrentConcurrentCount == ExecContext->TargetConcurrentCount) {
        ForWaitForProcessToComplete(ExecContext);
    }

Cleanup:

    YoriLibShFreeCmdContext(&NewCmd);
    YoriLibFreeStringContents(&CmdLine);
}

/**
 A callback that is invoked when a file is found that matches a search criteria
 specified in the set of strings to enumerate.

 @param FilePath Pointer to the file path that was found.

 @param FileInfo Information about the file.

 @param Depth Recursion depth, ignored in this application.

 @param Context The current state of the program and information needed to
        launch new child processes.

 @return TRUE to continute enumerating, FALSE to abort.
 */
BOOL
ForFileFoundCallback(
    __in PYORI_STRING FilePath,
    __in PWIN32_FIND_DATA FileInfo,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    PFOR_EXEC_CONTEXT ExecContext;

    UNREFERENCED_PARAMETER(Depth);
    UNREFERENCED_PARAMETER(FileInfo);

    ExecContext = (PFOR_EXEC_CONTEXT)Context;

    ASSERT(YoriLibIsStringNullTerminated(FilePath));

    if (!YoriLibFileFiltCheckFilterMatch(&ExecContext->Filter, FilePath, FileInfo)) {
        return TRUE;
    }

    ForExecuteCommand(FilePath, ExecContext);
    return TRUE;
}

#ifdef YORI_BUILTIN
/**
 The main entrypoint for the for builtin command.
 */
#define ENTRYPOINT YoriCmd_FOR
#else
/**
 The main entrypoint for the for standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the for cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the child process on success, or failure if the child
         could not be launched.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    YORI_STRING Arg;
    BOOL LeftBraceOpen;
    BOOL RequiresExpansion;
    BOOL MatchDirectories;
    BOOL Recurse;
    BOOL BasicEnumeration;
    BOOL StepMode;
    DWORD CharIndex;
    DWORD MatchFlags;
    DWORD StartArg = 0;
    DWORD ListArg = 0;
    DWORD CmdArg = 0;
    DWORD ArgIndex;
    DWORD i;
    FOR_EXEC_CONTEXT ExecContext;

    ZeroMemory(&ExecContext, sizeof(ExecContext));

    ExecContext.TargetConcurrentCount = 1;
    ExecContext.CurrentConcurrentCount = 0;
    MatchDirectories = FALSE;
    Recurse = FALSE;
    StepMode = FALSE;
    BasicEnumeration = FALSE;

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                ForHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2017-2019"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("b")) == 0) {
                BasicEnumeration = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("c")) == 0) {
                ExecContext.InvokeCmd = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("d")) == 0) {
                MatchDirectories = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("i")) == 0) {
                if (i + 1 < ArgC) {
                    YORI_STRING ErrorSubstring;
                    YORI_LIB_FILE_FILTER Filter;
                    YoriLibInitEmptyString(&ErrorSubstring);

                    if (!YoriLibFileFiltParseFilterString(&Filter, &ArgV[i + 1], &ErrorSubstring)) {
                        if (ErrorSubstring.LengthInChars > 0) {
                            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("for: error parsing filter string '%y' at '%y'\n"), &ArgV[i + 1], &ErrorSubstring);
                        } else {
                            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("for: error parsing filter string '%y'\n"), &ArgV[i + 1]);
                        }
                        goto cleanup_and_exit;
                    }
                    memcpy(&ExecContext.Filter, &Filter, sizeof(YORI_LIB_FILE_FILTER));
                    i++;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("l")) == 0) {
                StepMode = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("p")) == 0) {
                if (i + 1 < ArgC) {
                    LONGLONG LlNumberProcesses = 0;
                    DWORD CharsConsumed = 0;
                    if (YoriLibStringToNumber(&ArgV[i + 1], TRUE, &LlNumberProcesses, &CharsConsumed) &&
                        CharsConsumed > 0) {

                        ExecContext.TargetConcurrentCount = (DWORD)LlNumberProcesses;
                        ArgumentUnderstood = TRUE;
                        if (ExecContext.TargetConcurrentCount < 1) {
                            ExecContext.TargetConcurrentCount = 1;
                        }
                        i++;
                    }
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("r")) == 0) {
                Recurse = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                ArgumentUnderstood = TRUE;
                StartArg = i + 1;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    if (StartArg == 0 || StartArg == ArgC) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("for: missing argument\n"));
        goto cleanup_and_exit;
    }

    ExecContext.SubstituteVariable = &ArgV[StartArg];

    //
    //  We need at least "%i in (*) do cmd"
    //

    if (ArgC < StartArg + 4) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("for: missing argument\n"));
        goto cleanup_and_exit;
    }

    if (YoriLibCompareStringWithLiteralInsensitive(&ArgV[StartArg + 1], _T("in")) != 0) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("for: 'in' not found\n"));
        goto cleanup_and_exit;
    }

    if (YoriLibCompareStringWithLiteralInsensitiveCount(&ArgV[StartArg + 2], _T("("), 1) != 0) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("for: left bracket not found\n"));
        goto cleanup_and_exit;
    }

    LeftBraceOpen = TRUE;

    //
    //  Walk through all arguments looking for a closing brace, then looking
    //  for "do".  Once we've done finding both, we have a trailing command
    //  string
    //

    ListArg = StartArg + 2;
    for (ArgIndex = ListArg; ArgIndex < ArgC; ArgIndex++) {
        if (LeftBraceOpen) {
            if (ArgV[ArgIndex].LengthInChars > 0 && ArgV[ArgIndex].StartOfString[ArgV[ArgIndex].LengthInChars - 1] == ')') {
                LeftBraceOpen = FALSE;
            }
        } else {
            if (YoriLibCompareStringWithLiteralInsensitive(&ArgV[ArgIndex], _T("do")) == 0) {
                CmdArg = ArgIndex + 1;
                break;
            }
        }
    }

    if (CmdArg == 0) {
        if (LeftBraceOpen) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("for: right bracket not found\n"));
            goto cleanup_and_exit;
        } else {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("for: do not found\n"));
            goto cleanup_and_exit;
        }
    }

    if (CmdArg >= ArgC) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("for: command not found\n"));
        goto cleanup_and_exit;
    }

    ExecContext.ArgC = ArgC - CmdArg;
    ExecContext.ArgV = &ArgV[CmdArg];
    ExecContext.HandleArray = YoriLibMalloc(ExecContext.TargetConcurrentCount * sizeof(HANDLE));
    if (ExecContext.HandleArray == NULL) {
        goto cleanup_and_exit;
    }

    MatchFlags = 0;
    if (MatchDirectories) {
        MatchFlags = YORILIB_FILEENUM_RETURN_DIRECTORIES;
    } else {
        MatchFlags = YORILIB_FILEENUM_RETURN_FILES;
    }

    if (Recurse) {
        MatchFlags |= YORILIB_FILEENUM_RECURSE_AFTER_RETURN | YORILIB_FILEENUM_RECURSE_PRESERVE_WILD;
    }
    if (BasicEnumeration) {
        MatchFlags |= YORILIB_FILEENUM_BASIC_EXPANSION;
    }

    if (StepMode) {
        LONGLONG Start;
        LONGLONG Step;
        LONGLONG End;
        LONGLONG Current;
        DWORD CharsConsumed;
        YORI_STRING FoundMatch;
        YORI_STRING Criteria;

        if (!YoriLibBuildCmdlineFromArgcArgv(CmdArg - ListArg, &ArgV[ListArg], FALSE, TRUE, &Criteria)) {
            goto cleanup_and_exit;
        }

        //
        //  Remove the brackets
        //

        Criteria.StartOfString++;
        Criteria.LengthInChars -= 2;
        YoriLibTrimSpaces(&Criteria);

        //
        //  Get the start value
        //

        if (!YoriLibStringToNumber(&Criteria, FALSE, &Start, &CharsConsumed)) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("for: argument not numeric\n"));
            YoriLibFreeStringContents(&Criteria);
            goto cleanup_and_exit;
        }

        Criteria.StartOfString += CharsConsumed;
        Criteria.LengthInChars -= CharsConsumed;
        YoriLibTrimSpaces(&Criteria);

        if (Criteria.StartOfString[0] != ',') {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("for: seperator not found\n"));

            YoriLibFreeStringContents(&Criteria);
            goto cleanup_and_exit;
        }

        Criteria.StartOfString += 1;
        Criteria.LengthInChars -= 1;
        YoriLibTrimSpaces(&Criteria);

        //
        //  Get the step value
        //

        if (!YoriLibStringToNumber(&Criteria, FALSE, &Step, &CharsConsumed)) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("for: argument not numeric\n"));
            YoriLibFreeStringContents(&Criteria);
            goto cleanup_and_exit;
        }

        Criteria.StartOfString += CharsConsumed;
        Criteria.LengthInChars -= CharsConsumed;
        YoriLibTrimSpaces(&Criteria);

        if (Criteria.StartOfString[0] != ',') {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("for: seperator not found\n"));

            YoriLibFreeStringContents(&Criteria);
            goto cleanup_and_exit;
        }

        Criteria.StartOfString += 1;
        Criteria.LengthInChars -= 1;
        YoriLibTrimSpaces(&Criteria);

        //
        //  Get the end value
        //

        if (!YoriLibStringToNumber(&Criteria, FALSE, &End, &CharsConsumed)) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("for: argument not numeric\n"));
            YoriLibFreeStringContents(&Criteria);
            goto cleanup_and_exit;
        }

        YoriLibFreeStringContents(&Criteria);

        if (!YoriLibAllocateString(&FoundMatch, 32)) {
            goto cleanup_and_exit;
        }

        Current = Start;

        do {
            if (Step > 0) {
                if (Current > End) {
                    break;
                }
            } else if (Step < 0) {
                if (Current < End) {
                    break;
                }
            }

            YoriLibNumberToString(&FoundMatch, Current, 10, 0, '\0');
            ForExecuteCommand(&FoundMatch, &ExecContext);
            Current += Step;
        } while(TRUE);

        YoriLibFreeStringContents(&FoundMatch);

    } else {

        for (ArgIndex = ListArg; ArgIndex < CmdArg - 1; ArgIndex++) {
            YORI_STRING ThisMatch;

            YoriLibInitEmptyString(&ThisMatch);

            ThisMatch.StartOfString = ArgV[ArgIndex].StartOfString;
            ThisMatch.LengthInChars = ArgV[ArgIndex].LengthInChars;
            ThisMatch.LengthAllocated = ArgV[ArgIndex].LengthAllocated;

            if (ArgIndex == ListArg) {
                ThisMatch.StartOfString++;
                ThisMatch.LengthInChars--;
                ThisMatch.LengthAllocated--;
            }

            if (ArgIndex == CmdArg - 2) {
                LPTSTR NullTerminatedString;

                ThisMatch.LengthInChars--;
                ThisMatch.LengthAllocated--;
                NullTerminatedString = YoriLibCStringFromYoriString(&ThisMatch);
                if (NullTerminatedString == NULL) {
                    goto cleanup_and_exit;
                }
                ThisMatch.MemoryToFree = NullTerminatedString;
                ThisMatch.StartOfString = NullTerminatedString;
            }

            RequiresExpansion = FALSE;
            if (Recurse) {
                RequiresExpansion = TRUE;
            } else {
                for (CharIndex = 0; CharIndex < ThisMatch.LengthInChars; CharIndex++) {
                    if (ThisMatch.StartOfString[CharIndex] == '*' ||
                        ThisMatch.StartOfString[CharIndex] == '?') {

                        RequiresExpansion = TRUE;
                        break;
                    }
                    if (!BasicEnumeration) {
                        if (ThisMatch.StartOfString[CharIndex] == '[' ||
                            ThisMatch.StartOfString[CharIndex] == '{') {

                            RequiresExpansion = TRUE;
                            break;
                        }
                    }
                }
            }

            if (ThisMatch.LengthInChars > 0) {

                if (RequiresExpansion) {
                    YoriLibForEachFile(&ThisMatch, MatchFlags, 0, ForFileFoundCallback, NULL, &ExecContext);
                } else {
                    ForExecuteCommand(&ThisMatch, &ExecContext);
                }
            }

            //
            //  Because MemoryToFree is not normally populated, this only
            //  really frees where the memory was double buffered
            //

            YoriLibFreeStringContents(&ThisMatch);
        }
    }

    while (ExecContext.CurrentConcurrentCount > 0) {
        ForWaitForProcessToComplete(&ExecContext);
    }

    YoriLibFileFiltFreeFilter(&ExecContext.Filter);
    YoriLibFree(ExecContext.HandleArray);

    return EXIT_SUCCESS;

cleanup_and_exit:

    YoriLibFileFiltFreeFilter(&ExecContext.Filter);

    return EXIT_FAILURE;
}

// vim:sw=4:ts=4:et:


<|start_filename|>cvtvt/rtf.c<|end_filename|>
/**
 * @file cvtvt/rtf.c
 *
 * Convert VT100/ANSI escape sequences into RTF.
 *
 * Copyright (c) 2015-2018 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "cvtvt.h"

/**
 While parsing, TRUE if underlining is in effect.  This global is updated
 when text is generated.
 */
BOOLEAN CvtvtRtfUnderlineOn = FALSE;

/**
 Indicate the beginning of a stream and perform any initial output.

 @param hOutput The stream to output any footer information to.

 @param Context Pointer to context (unused.)

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
CvtvtRtfInitializeStream(
    __in HANDLE hOutput,
    __in PDWORDLONG Context
    )
{
    YORI_STRING OutputString;

    UNREFERENCED_PARAMETER(Context);

    YoriLibInitEmptyString(&OutputString);
    if (!YoriLibRtfGenerateInitialString(&OutputString, NULL)) {
        return FALSE;
    }

    YoriLibOutputTextToMultibyteDevice(hOutput, &OutputString);
    YoriLibFreeStringContents(&OutputString);
    return TRUE;
}

/**
 Indicate the end of the stream has been reached and perform any final
 output.

 @param hOutput The stream to output any footer information to.

 @param Context Pointer to context (unused.)

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
CvtvtRtfEndStream(
    __in HANDLE hOutput,
    __in PDWORDLONG Context
    )
{
    YORI_STRING OutputString;
    UNREFERENCED_PARAMETER(Context);
    YoriLibInitEmptyString(&OutputString);

    if (!YoriLibRtfGenerateEndString(&OutputString)) {
        return FALSE;
    }

    YoriLibOutputTextToMultibyteDevice(hOutput, &OutputString);
    YoriLibFreeStringContents(&OutputString);

    return TRUE;
}


/**
 Parse text between VT100 escape sequences and generate correct output for
 RTF.

 @param hOutput The output stream to populate with the translated text
        information.

 @param String Pointer to a string buffer containing the text to process.

 @param Context Pointer to context (unused.)

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
CvtvtRtfProcessAndOutputText(
    __in HANDLE hOutput,
    __in PCYORI_STRING String,
    __in PDWORDLONG Context
    )
{

    YORI_STRING TextString;
    DWORD BufferSizeNeeded;

    UNREFERENCED_PARAMETER(Context);

    YoriLibInitEmptyString(&TextString);
    BufferSizeNeeded = 0;
    if (!YoriLibRtfGenerateTextString(&TextString, &BufferSizeNeeded, String)) {
        return FALSE;
    }

    if (!YoriLibAllocateString(&TextString, BufferSizeNeeded)) {
        return FALSE;
    }

    BufferSizeNeeded = 0;
    if (!YoriLibRtfGenerateTextString(&TextString, &BufferSizeNeeded, String)) {
        YoriLibFreeStringContents(&TextString);
        return FALSE;
    }

    YoriLibOutputTextToMultibyteDevice(hOutput, &TextString);

    YoriLibFreeStringContents(&TextString);
    return TRUE;
}


/**
 Parse a VT100 escape sequence and generate the correct output for RTF.

 @param hOutput The output stream to populate with the translated escape
        information.

 @param String Pointer to a string buffer containing the escape to process.

 @param Context Pointer to context (unused.)

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
CvtvtRtfProcessAndOutputEscape(
    __in HANDLE hOutput,
    __in PCYORI_STRING String,
    __in PDWORDLONG Context
    )
{
    YORI_STRING TextString;
    DWORD BufferSizeNeeded;
    BOOLEAN DummyUnderlineState;

    UNREFERENCED_PARAMETER(Context);

    YoriLibInitEmptyString(&TextString);
    BufferSizeNeeded = 0;
    DummyUnderlineState = CvtvtRtfUnderlineOn;
    if (!YoriLibRtfGenerateEscapeString(&TextString, &BufferSizeNeeded, String, &DummyUnderlineState)) {
        return FALSE;
    }

    if (!YoriLibAllocateString(&TextString, BufferSizeNeeded)) {
        return FALSE;
    }

    BufferSizeNeeded = 0;
    if (!YoriLibRtfGenerateEscapeString(&TextString, &BufferSizeNeeded, String, &CvtvtRtfUnderlineOn)) {
        YoriLibFreeStringContents(&TextString);
        return FALSE;
    }

    YoriLibOutputTextToMultibyteDevice(hOutput, &TextString);

    YoriLibFreeStringContents(&TextString);
    return TRUE;
}

/**
 Set parsing functions to generate RTF output.

 @param CallbackFunctions Function pointers to populate with those that can
        generate RTF output.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
CvtvtRtfSetFunctions(
    __out PYORI_LIB_VT_CALLBACK_FUNCTIONS CallbackFunctions
    )
{
    CallbackFunctions->InitializeStream = CvtvtRtfInitializeStream;
    CallbackFunctions->EndStream = CvtvtRtfEndStream;
    CallbackFunctions->ProcessAndOutputText = CvtvtRtfProcessAndOutputText;
    CallbackFunctions->ProcessAndOutputEscape = CvtvtRtfProcessAndOutputEscape;
    return TRUE;
}

// vim:sw=4:ts=4:et:


<|start_filename|>fscmp/fscmp.c<|end_filename|>
/**
 * @file fscmp/fscmp.c
 *
 * Yori shell test file system state
 *
 * Copyright (c) 2018 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 Help text to display to the user.
 */
const
CHAR strFsCmpHelpText[] =
        "\n"
        "Test for file system conditions.\n"
        "\n"
        "FSCMP [-license] [-b] [-d | -e | -f | -i <condition> | -l] <file>\n"
        "\n"
        "   -b             Use basic search criteria\n"
        "   -d             Test if directory exists\n"
        "   -e             Test if object exists\n"
        "   -f             Test if file exists\n"
        "   -i             Test for a specified file metadata condition\n"
        "   -l             Test if symbolic link exists\n"
        "\n"
        " The -i option will match files only if they meet criteria.  This is a\n"
        " semicolon delimited list of entries matching the following form:\n"
        "\n"
        "   [file attribute][operator][criteria]\n";

/**
 Display usage text to the user.
 */
BOOL
FsCmpHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("FsCmp %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strFsCmpHelpText);

    //
    //  Display supported options and operators
    //

    YoriLibFileFiltHelp();
    return TRUE;
}

/**
 An enum of tests that can be applied to files.
 */
typedef enum _FSCMP_TEST_TYPE {
    FsCmpTestTypeUnknown = 0,
    FsCmpTestTypeExists = 1,
    FsCmpTestTypeDirectoryExists = 2,
    FsCmpTestTypeFileExists = 3,
    FsCmpTestTypeLinkExists = 4,
    FsCmpTestTypeApplyFilter = 5,
} FSCMP_TEST_TYPE;

/**
 Context to pass when enumerating files.
 */
typedef struct _FSCMP_CONTEXT {

    /**
     The test to apply.
     */
    FSCMP_TEST_TYPE TestType;

    /**
     Set to TRUE if the test is met.
     */
    BOOL ConditionMet;

    /**
     A list of criteria to filter matches against.
     */
    YORI_LIB_FILE_FILTER Filter;
} FSCMP_CONTEXT, *PFSCMP_CONTEXT;

/**
 A callback that is invoked when a file is found that matches a search criteria
 specified in the set of strings to enumerate.

 @param FilePath Pointer to the file path that was found.

 @param FileInfo Information about the file.

 @param Depth Recursion depth, ignored in this application.

 @param Context Pointer to an FSCMP_CONTEXT.

 @return TRUE to continute enumerating, FALSE to abort.
 */
BOOL
FsCmpFileFoundCallback(
    __in PYORI_STRING FilePath,
    __in PWIN32_FIND_DATA FileInfo,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    PFSCMP_CONTEXT FsCmpContext = (PFSCMP_CONTEXT)Context;

    UNREFERENCED_PARAMETER(Depth);
    UNREFERENCED_PARAMETER(FilePath);

    if (FsCmpContext->TestType == FsCmpTestTypeExists) {
        FsCmpContext->ConditionMet = TRUE;
        return FALSE;
    } else if (FsCmpContext->TestType == FsCmpTestTypeDirectoryExists) {
        if (FileInfo->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
            FsCmpContext->ConditionMet = TRUE;
            return FALSE;
        }
    } else if (FsCmpContext->TestType == FsCmpTestTypeLinkExists) {
        if ((FileInfo->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0 &&
            (FileInfo->dwReserved0 == IO_REPARSE_TAG_MOUNT_POINT ||
             FileInfo->dwReserved0 == IO_REPARSE_TAG_SYMLINK)) {

            FsCmpContext->ConditionMet = TRUE;
            return FALSE;
        }
    } else if (FsCmpContext->TestType == FsCmpTestTypeFileExists) {
        if ((FileInfo->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0 &&
            ((FileInfo->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0 ||
             (FileInfo->dwReserved0 != IO_REPARSE_TAG_MOUNT_POINT &&
              FileInfo->dwReserved0 != IO_REPARSE_TAG_SYMLINK))) {

            FsCmpContext->ConditionMet = TRUE;
            return FALSE;
        }
    } else if (FsCmpContext->TestType == FsCmpTestTypeApplyFilter) {
        if (YoriLibFileFiltCheckFilterMatch(&FsCmpContext->Filter, FilePath, FileInfo)) {
            FsCmpContext->ConditionMet = TRUE;
            return FALSE;
        }
    }

    return TRUE;
}

#ifdef YORI_BUILTIN
/**
 The main entrypoint for the fscmp builtin command.
 */
#define ENTRYPOINT YoriCmd_FSCMP
#else
/**
 The main entrypoint for the fscmp standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the fscmp cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the child process on success, or failure if the child
         could not be launched.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD StartArg = 0;
    DWORD i;
    DWORD MatchFlags;
    FSCMP_CONTEXT FsCmpContext;
    BOOL BasicExpansion;
    YORI_STRING Arg;

    ZeroMemory(&FsCmpContext, sizeof(FsCmpContext));
    BasicExpansion = FALSE;

    for (i = 1; i < ArgC; i++) {

        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));
        ArgumentUnderstood = FALSE;

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                FsCmpHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2018"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("b")) == 0) {
                BasicExpansion = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("d")) == 0) {
                FsCmpContext.TestType = FsCmpTestTypeDirectoryExists;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("e")) == 0) {
                FsCmpContext.TestType = FsCmpTestTypeExists;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("f")) == 0) {
                FsCmpContext.TestType = FsCmpTestTypeFileExists;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("i")) == 0) {
                if (i + 1 < ArgC) {
                    YORI_STRING ErrorSubstring;
                    YORI_LIB_FILE_FILTER Filter;
                    YoriLibInitEmptyString(&ErrorSubstring);

                    YoriLibFileFiltFreeFilter(&FsCmpContext.Filter);
                    if (!YoriLibFileFiltParseFilterString(&Filter, &ArgV[i + 1], &ErrorSubstring)) {
                        if (ErrorSubstring.LengthInChars > 0) {
                            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("fscmp: error parsing filter string '%y' at '%y'\n"), &ArgV[i + 1], &ErrorSubstring);
                        } else {
                            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("fscmp: error parsing filter string '%y'\n"), &ArgV[i + 1]);
                        }
                        goto cleanup_and_exit;
                    }
                    memcpy(&FsCmpContext.Filter, &Filter, sizeof(Filter));
                    FsCmpContext.TestType = FsCmpTestTypeApplyFilter;
                    i++;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("l")) == 0) {
                FsCmpContext.TestType = FsCmpTestTypeLinkExists;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                ArgumentUnderstood = TRUE;
                StartArg = i + 1;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    if (StartArg == 0 || StartArg == ArgC) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("fscmp: missing argument\n"));
        goto cleanup_and_exit;
    }

    if (FsCmpContext.TestType == FsCmpTestTypeUnknown) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("fscmp: missing test condition\n"));
        goto cleanup_and_exit;
    }

    MatchFlags = YORILIB_FILEENUM_RETURN_FILES | YORILIB_FILEENUM_RETURN_DIRECTORIES;
    if (BasicExpansion) {
        MatchFlags |= YORILIB_FILEENUM_BASIC_EXPANSION;
    }

    for (i = StartArg; i < ArgC && !FsCmpContext.ConditionMet; i++) {
        YoriLibForEachFile(&ArgV[i], MatchFlags, 0, FsCmpFileFoundCallback, NULL, &FsCmpContext);
    }

cleanup_and_exit:

    YoriLibFileFiltFreeFilter(&FsCmpContext.Filter);

    if (FsCmpContext.ConditionMet) {
        return EXIT_SUCCESS;
    }
    return EXIT_FAILURE;
}

// vim:sw=4:ts=4:et:


<|start_filename|>vhdtool/vhdtool.c<|end_filename|>
/**
 * @file vhdtool/vhdtool.c
 *
 * Yori shell vhdtool for managing VHD files
 *
 * Copyright (c) 2019 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 Help text to display to the user.
 */
const
CHAR strVhdToolHelpText[] =
        "\n"
        "Manage VHD files.\n"
        "\n"
        "VHDTOOL [-license]\n"
        "VHDTOOL [-sector:512|-sector:512e|-sector:4096] -clonedynamic <file> <source>\n"
        "VHDTOOL [-sector:512|-sector:512e|-sector:4096] -clonefixed <file> <source>\n"
        "VHDTOOL -compact <file>\n"
        "VHDTOOL -creatediff <file> <parent>\n"
        "VHDTOOL [-sector:512|-sector:512e|-sector:4096] -createdynamic <file> <size>\n"
        "VHDTOOL [-sector:512|-sector:512e|-sector:4096] -createfixed <file> <size>\n"
        "VHDTOOL -expand <file> <size>\n"
        "VHDTOOL -merge <file>\n"
        "VHDTOOL -shrink <file> <size>\n"
        "\n"
        "   -clonedynamic  Copy an existing disk or VHD into a dynamically expanding\n"
        "                  .vhd or .vhdx file\n"
        "   -clonefixed    Copy an existing disk or VHD into a fixed sized .iso, .vhd\n"
        "                  or .vhdx file\n"
        "   -compact       Remove unused regions from a dynamically expanding .vhd or\n"
        "                  .vhdx file\n"
        "   -creatediff    Create a differencing .vhd or .vhdx file from a read-only\n"
        "                  parent .vhd or .vhdx file\n"
        "   -createdynamic Create a new dynamically expanding .vhd or .vhdx file.  Size\n"
        "                  can be specified with a 'k', 'm', 'g', or 't' suffix.\n"
        "   -createfixed   Create a new fixed size .vhd or .vhdx file.  Size can be\n"
        "                  specified with a 'k', 'm', 'g', or 't' suffix.\n"
        "   -expand        Increase the size of a .vhd or .vhdx file.  Size can be\n"
        "                  specified with a 'k', 'm', 'g', or 't' suffix.\n"
        "   -merge         Merge a differencing .vhd or .vhdx file into its parent\n"
        "   -shrink        Decrease the size of a .vhdx file.  Size can be specified\n"
        "                  with a 'k', 'm', 'g', or 't' suffix.\n";

/**
 Display usage text to the user.
 */
BOOL
VhdToolHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("VhdTool %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strVhdToolHelpText);
    return TRUE;
}

/**
 A set of detected file types supported by this application.
 */
typedef enum _VHDTOOL_EXT_TYPE {
    VhdToolExtUnknown = 0,
    VhdToolExtVhd = 1,
    VhdToolExtVhdx = 2,
    VhdToolExtIso = 3,
} VHDTOOL_EXT_TYPE;

/**
 A set of supported sector sizes.
 */
typedef enum _VHDTOOL_SECTOR_SIZE {
    VhdToolSectorDefault = 0,
    VhdToolSector512Native = 1,
    VhdToolSector512e = 2,
    VhdToolSector4kNative = 3
} VHDTOOL_SECTOR_SIZE;

/**
 Clone a fixed ISO file.

 @param Path Pointer to the path of the file to create.

 @param SourcePath Pointer to the path of the device to populate from.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
VhdToolCloneIso(
    __in PYORI_STRING Path,
    __in PYORI_STRING SourcePath
    )
{
    HANDLE SourceHandle;
    HANDLE TargetHandle;
    YORI_STRING FullPath;
    YORI_STRING FullSourcePath;
    PVOID Buffer;
    DWORD BufferSize;
    DWORD BytesRead;
    DWORD SectorsPerCluster;
    DWORD BytesPerSector;
    DWORD FreeClusters;
    DWORD TotalClusters;
    DISK_GEOMETRY DiskGeometry;
    LPTSTR ErrText;
    DWORD Err;

    YoriLibInitEmptyString(&FullPath);
    YoriLibInitEmptyString(&FullSourcePath);

    if (!YoriLibUserStringToSingleFilePath(Path, TRUE, &FullPath)) {
        return FALSE;
    }

    if (!YoriLibUserStringToSingleFilePath(SourcePath, TRUE, &FullSourcePath)) {
        YoriLibFreeStringContents(&FullPath);
        return FALSE;
    }

    //
    //  Open the source.  Note this can be a file or a device.
    //

    SourceHandle = CreateFile(FullSourcePath.StartOfString, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (SourceHandle == INVALID_HANDLE_VALUE) {
        Err = GetLastError();
        ErrText = YoriLibGetWinErrorText(Err);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Open of source failed: %y: %s"), &FullSourcePath, ErrText);
        YoriLibFreeStringContents(&FullPath);
        YoriLibFreeStringContents(&FullSourcePath);
        YoriLibFreeWinErrorText(ErrText);
        return FALSE;
    }

    TargetHandle = CreateFile(FullPath.StartOfString, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_DELETE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if (SourceHandle == INVALID_HANDLE_VALUE) {
        Err = GetLastError();
        ErrText = YoriLibGetWinErrorText(Err);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Open of target failed: %y: %s"), &FullPath, ErrText);
        YoriLibFreeStringContents(&FullPath);
        YoriLibFreeStringContents(&FullSourcePath);
        CloseHandle(SourceHandle);
        YoriLibFreeWinErrorText(ErrText);
        return FALSE;
    }

    //
    //  Try to query the sector size of the source, first as a device, and if
    //  that fails, as a file.
    //

    if (DeviceIoControl(SourceHandle, IOCTL_DISK_GET_DRIVE_GEOMETRY, NULL, 0, &DiskGeometry, sizeof(DiskGeometry), &BytesRead, NULL)) {
        BytesPerSector = DiskGeometry.BytesPerSector;
    } else if (!GetDiskFreeSpace(FullSourcePath.StartOfString, &SectorsPerCluster, &BytesPerSector, &FreeClusters, &TotalClusters)) {
        BytesPerSector = 4096;
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("BytesPerSector could not be detected, using default %i\n"), BytesPerSector);
    }

    BufferSize = 1024 * 1024;
    Buffer = YoriLibMalloc(BufferSize);
    if (Buffer == NULL) {
        YoriLibFreeStringContents(&FullPath);
        YoriLibFreeStringContents(&FullSourcePath);
        CloseHandle(SourceHandle);
        CloseHandle(TargetHandle);
        return FALSE;
    }

    //
    //  Copy data.  Devices appear to fail outright if the end of the device
    //  is reached, so the final portion is read one sector at a time.
    //

    while(TRUE) {
        if (!ReadFile(SourceHandle, Buffer, BufferSize, &BytesRead, NULL)) {
            Err = GetLastError();
            if (Err == ERROR_INVALID_FUNCTION) {
                if (BufferSize != BytesPerSector) {
                    BufferSize = BytesPerSector;
                    continue;
                }
            }
            break;
        }

        if (BytesRead == 0) {
            break;
        }

        WriteFile(TargetHandle, Buffer, BytesRead, &BytesRead, NULL);
    }

    YoriLibFree(Buffer);
    YoriLibFreeStringContents(&FullPath);
    YoriLibFreeStringContents(&FullSourcePath);
    CloseHandle(SourceHandle);
    CloseHandle(TargetHandle);
    return TRUE;
}

/**
 Create a fixed or dynamic VHD file.

 @param Path Pointer to the path of the file to create.

 @param SizeAsString Pointer to a string form of the size of the file to
        create.

 @param Fixed If TRUE, the VHD file is created fully allocated.  If not, the
        file is expanded as needed.

 @param ExtType The extension type for the virtual disk.

 @param SourceFile Optionally points to a source to populate from.  Note
        that if this is specified SizeAsString is meaningless.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
VhdToolCreateNewVhd(
    __in PYORI_STRING Path,
    __in_opt PYORI_STRING SizeAsString,
    __in BOOL Fixed,
    __in VHDTOOL_EXT_TYPE ExtType,
    __in_opt PYORI_STRING SourceFile
    )
{
    LARGE_INTEGER FileSize;
    CREATE_VIRTUAL_DISK_PARAMETERS CreateParams;
    VIRTUAL_STORAGE_TYPE StorageType;
    HANDLE Handle;
    DWORD Flags;
    DWORD Err;
    YORI_STRING FullPath;
    YORI_STRING FullSourcePath;

    YoriLibLoadVirtDiskFunctions();
    if (DllVirtDisk.pCreateVirtualDisk == NULL) {

        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("vhdtool: OS support not present\n"));
        return FALSE;
    }

    if (ExtType == VhdToolExtIso) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("vhdtool: cannot create empty ISO\n"));
        return FALSE;
    }

    ZeroMemory(&CreateParams, sizeof(CreateParams));
    ZeroMemory(&StorageType, sizeof(StorageType));

    YoriLibInitEmptyString(&FullPath);
    YoriLibInitEmptyString(&FullSourcePath);

    if (!YoriLibUserStringToSingleFilePath(Path, TRUE, &FullPath)) {
        return FALSE;
    }

    CreateParams.Version = 1;
    if (SizeAsString != NULL) {
        FileSize = YoriLibStringToFileSize(SizeAsString);
        CreateParams.Version1.MaximumSize = FileSize.QuadPart;
    }
    CreateParams.Version1.BlockSizeInBytes = 0;
    CreateParams.Version1.SectorSizeInBytes = 0x200;
    CreateParams.Version1.ParentPath = NULL;
    if (SourceFile != NULL) {
        if (!YoriLibUserStringToSingleFilePath(SourceFile, TRUE, &FullSourcePath)) {
            YoriLibFreeStringContents(&FullPath);
            return FALSE;
        }
        CreateParams.Version1.SourcePath = FullSourcePath.StartOfString;
    } else {
        CreateParams.Version1.SourcePath = NULL;
    }

    StorageType.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_VHD;
    memcpy(&StorageType.VendorId, &VIRTUAL_STORAGE_TYPE_VENDOR_MICROSOFT, sizeof(GUID));

    Flags = 0;
    if (Fixed) {
        Flags |= CREATE_VIRTUAL_DISK_FLAG_FULL_PHYSICAL_ALLOCATION;
    }

    Err = DllVirtDisk.pCreateVirtualDisk(&StorageType,
                                         FullPath.StartOfString,
                                         VIRTUAL_DISK_ACCESS_CREATE,
                                         NULL,
                                         Flags,
                                         0,
                                         &CreateParams,
                                         NULL,
                                         &Handle);

    if (Err != 0) {
        LPTSTR ErrText;
        ErrText = YoriLibGetWinErrorText(Err);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Create of disk failed: %y: %s"), &FullPath, ErrText);
        YoriLibFreeStringContents(&FullPath);
        YoriLibFreeStringContents(&FullSourcePath);
        YoriLibFreeWinErrorText(ErrText);
        return FALSE;
    }

    YoriLibFreeStringContents(&FullPath);
    YoriLibFreeStringContents(&FullSourcePath);
    CloseHandle(Handle);
    return TRUE;
}

/**
 Create a fixed or dynamic VHDX file.

 @param Path Pointer to the path of the file to create.

 @param SizeAsString Pointer to a string form of the size of the file to
        create.

 @param Fixed If TRUE, the VHD file is created fully allocated.  If not, the
        file is expanded as needed.

 @param SectorSize The size of each sector within the virtual disk.

 @param SourceFile Optionally points to a source to populate from.  Note
        that if this is specified SizeAsString is meaningless.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
VhdToolCreateNewVhdx(
    __in PYORI_STRING Path,
    __in_opt PYORI_STRING SizeAsString,
    __in BOOL Fixed,
    __in VHDTOOL_SECTOR_SIZE SectorSize,
    __in_opt PYORI_STRING SourceFile
    )
{
    LARGE_INTEGER FileSize;
    CREATE_VIRTUAL_DISK_PARAMETERS CreateParams;
    VIRTUAL_STORAGE_TYPE StorageType;
    HANDLE Handle;
    DWORD Flags;
    DWORD Err;
    YORI_STRING FullPath;
    YORI_STRING FullSourcePath;

    YoriLibLoadVirtDiskFunctions();
    if (DllVirtDisk.pCreateVirtualDisk == NULL) {

        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("vhdtool: OS support not present\n"));
        return FALSE;
    }

    ZeroMemory(&CreateParams, sizeof(CreateParams));
    ZeroMemory(&StorageType, sizeof(StorageType));

    YoriLibInitEmptyString(&FullPath);
    YoriLibInitEmptyString(&FullSourcePath);

    if (!YoriLibUserStringToSingleFilePath(Path, TRUE, &FullPath)) {
        return FALSE;
    }

    CreateParams.Version = 2;
    if (SizeAsString != NULL) {
        FileSize = YoriLibStringToFileSize(SizeAsString);
        CreateParams.Version2.MaximumSize = FileSize.QuadPart;
    }
    CreateParams.Version2.BlockSizeInBytes = 0;
    switch (SectorSize) {
        case VhdToolSector512e:
            CreateParams.Version2.SectorSizeInBytes = 0x200;
            CreateParams.Version2.PhysicalSectorSizeInBytes = 0x1000;
            break;
        case VhdToolSector4kNative:
            CreateParams.Version2.SectorSizeInBytes = 0x1000;
            CreateParams.Version2.PhysicalSectorSizeInBytes = 0x1000;
            break;
        default:
            CreateParams.Version2.SectorSizeInBytes = 0x200;
            CreateParams.Version2.PhysicalSectorSizeInBytes = 0x200;
            break;
    }
    CreateParams.Version2.ParentPath = NULL;
    if (SourceFile != NULL) {
        if (!YoriLibUserStringToSingleFilePath(SourceFile, TRUE, &FullSourcePath)) {
            return FALSE;
        }
        CreateParams.Version2.SourcePath = FullSourcePath.StartOfString;
    } else {
        CreateParams.Version2.SourcePath = NULL;
    }
    CreateParams.Version2.ParentVirtualStorageType = 0;
    CreateParams.Version2.SourceVirtualStorageType = 0;

    StorageType.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_VHDX;
    memcpy(&StorageType.VendorId, &VIRTUAL_STORAGE_TYPE_VENDOR_MICROSOFT, sizeof(GUID));

    Flags = 0;
    if (Fixed) {
        Flags |= CREATE_VIRTUAL_DISK_FLAG_FULL_PHYSICAL_ALLOCATION;
    }

    Err = DllVirtDisk.pCreateVirtualDisk(&StorageType,
                                         FullPath.StartOfString,
                                         0,
                                         NULL,
                                         Flags,
                                         0,
                                         &CreateParams,
                                         NULL,
                                         &Handle);

    if (Err != 0) {
        LPTSTR ErrText;
        ErrText = YoriLibGetWinErrorText(Err);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Create of disk failed: %y: %s"), &FullPath, ErrText);
        YoriLibFreeStringContents(&FullPath);
        YoriLibFreeStringContents(&FullSourcePath);
        YoriLibFreeWinErrorText(ErrText);
        return FALSE;
    }

    YoriLibFreeStringContents(&FullPath);
    YoriLibFreeStringContents(&FullSourcePath);
    CloseHandle(Handle);
    return TRUE;
}

/**
 Create a differencing VHD file.

 @param Path Pointer to the path of the file to create.

 @param ParentPath Pointer to the path of the parent.

 @param ExtType The extension type for the virtual disk.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
VhdToolCreateDiffVhd(
    __in PYORI_STRING Path,
    __in PYORI_STRING ParentPath,
    __in VHDTOOL_EXT_TYPE ExtType
    )
{
    CREATE_VIRTUAL_DISK_PARAMETERS CreateParams;
    VIRTUAL_STORAGE_TYPE StorageType;
    HANDLE Handle;
    DWORD Flags;
    DWORD Err;
    YORI_STRING FullPath;
    YORI_STRING FullParentPath;

    YoriLibLoadVirtDiskFunctions();
    if (DllVirtDisk.pCreateVirtualDisk == NULL) {

        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("vhdtool: OS support not present\n"));
        return FALSE;
    }

    if (ExtType == VhdToolExtIso) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("vhdtool: cannot create differencing ISO\n"));
        return FALSE;
    }

    ZeroMemory(&CreateParams, sizeof(CreateParams));
    ZeroMemory(&StorageType, sizeof(StorageType));

    YoriLibInitEmptyString(&FullPath);
    YoriLibInitEmptyString(&FullParentPath);

    if (!YoriLibUserStringToSingleFilePath(Path, TRUE, &FullPath)) {
        return FALSE;
    }

    if (!YoriLibUserStringToSingleFilePath(ParentPath, TRUE, &FullParentPath)) {
        YoriLibFreeStringContents(&FullPath);
        return FALSE;
    }

    CreateParams.Version = 1;
    CreateParams.Version1.MaximumSize = 0;
    CreateParams.Version1.BlockSizeInBytes = 0;
    CreateParams.Version1.SectorSizeInBytes = 0x200;
    CreateParams.Version1.ParentPath = FullParentPath.StartOfString;
    CreateParams.Version1.SourcePath = NULL;

    StorageType.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_VHD;
    memcpy(&StorageType.VendorId, &VIRTUAL_STORAGE_TYPE_VENDOR_MICROSOFT, sizeof(GUID));

    Flags = 0;

    Err = DllVirtDisk.pCreateVirtualDisk(&StorageType,
                                         FullPath.StartOfString,
                                         VIRTUAL_DISK_ACCESS_CREATE,
                                         NULL,
                                         Flags,
                                         0,
                                         &CreateParams,
                                         NULL,
                                         &Handle);

    if (Err != 0) {
        LPTSTR ErrText;
        ErrText = YoriLibGetWinErrorText(Err);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Create of disk failed: %y: %s"), &FullPath, ErrText);
        YoriLibFreeStringContents(&FullPath);
        YoriLibFreeStringContents(&FullParentPath);
        YoriLibFreeWinErrorText(ErrText);
        return FALSE;
    }

    YoriLibFreeStringContents(&FullPath);
    YoriLibFreeStringContents(&FullParentPath);
    CloseHandle(Handle);
    return TRUE;
}

/**
 Create a differencing VHDX file.

 @param Path Pointer to the path of the file to create.

 @param ParentPath Pointer to the path of the parent.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
VhdToolCreateDiffVhdx(
    __in PYORI_STRING Path,
    __in PYORI_STRING ParentPath
    )
{
    CREATE_VIRTUAL_DISK_PARAMETERS CreateParams;
    VIRTUAL_STORAGE_TYPE StorageType;
    HANDLE Handle;
    DWORD Flags;
    DWORD Err;
    YORI_STRING FullPath;
    YORI_STRING FullParentPath;

    YoriLibLoadVirtDiskFunctions();
    if (DllVirtDisk.pCreateVirtualDisk == NULL) {

        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("vhdtool: OS support not present\n"));
        return FALSE;
    }

    ZeroMemory(&CreateParams, sizeof(CreateParams));
    ZeroMemory(&StorageType, sizeof(StorageType));

    YoriLibInitEmptyString(&FullPath);
    YoriLibInitEmptyString(&FullParentPath);

    if (!YoriLibUserStringToSingleFilePath(Path, TRUE, &FullPath)) {
        return FALSE;
    }

    if (!YoriLibUserStringToSingleFilePath(ParentPath, TRUE, &FullParentPath)) {
        YoriLibFreeStringContents(&FullPath);
        return FALSE;
    }

    CreateParams.Version = 2;
    CreateParams.Version2.MaximumSize = 0;
    CreateParams.Version2.BlockSizeInBytes = 0;
    CreateParams.Version2.SectorSizeInBytes = 0;
    CreateParams.Version2.PhysicalSectorSizeInBytes = 0;
    CreateParams.Version2.ParentPath = FullParentPath.StartOfString;
    CreateParams.Version2.SourcePath = NULL;
    CreateParams.Version2.ParentVirtualStorageType = 0;
    CreateParams.Version2.SourceVirtualStorageType = 0;

    StorageType.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_VHDX;
    memcpy(&StorageType.VendorId, &VIRTUAL_STORAGE_TYPE_VENDOR_MICROSOFT, sizeof(GUID));

    Flags = 0;

    Err = DllVirtDisk.pCreateVirtualDisk(&StorageType,
                                         FullPath.StartOfString,
                                         0,
                                         NULL,
                                         Flags,
                                         0,
                                         &CreateParams,
                                         NULL,
                                         &Handle);

    if (Err != 0) {
        LPTSTR ErrText;
        ErrText = YoriLibGetWinErrorText(Err);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Create of disk failed: %y: %s"), &FullPath, ErrText);
        YoriLibFreeStringContents(&FullPath);
        YoriLibFreeStringContents(&FullParentPath);
        YoriLibFreeWinErrorText(ErrText);
        return FALSE;
    }

    YoriLibFreeStringContents(&FullPath);
    YoriLibFreeStringContents(&FullParentPath);
    CloseHandle(Handle);
    return TRUE;
}

/**
 Expand a VHD to a larger size.

 @param Path Pointer to the path of the file to expand.

 @param SizeAsString Pointer to a string form of the size of the file to
        expand to.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
VhdToolExpand(
    __in PYORI_STRING Path,
    __in PYORI_STRING SizeAsString
    )
{
    LARGE_INTEGER FileSize;
    VIRTUAL_STORAGE_TYPE StorageType;
    HANDLE Handle;
    YORI_STRING FullPath;
    OPEN_VIRTUAL_DISK_PARAMETERS OpenParams;
    EXPAND_VIRTUAL_DISK_PARAMETERS ExpandParams;
    DWORD Err;
    DWORD AccessRequested;
    LPTSTR ErrText;

    YoriLibLoadVirtDiskFunctions();
    if (DllVirtDisk.pExpandVirtualDisk == NULL ||
        DllVirtDisk.pOpenVirtualDisk == NULL) {

        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("vhdtool: OS support not present\n"));
        return FALSE;
    }

    ZeroMemory(&StorageType, sizeof(StorageType));

    YoriLibInitEmptyString(&FullPath);

    if (!YoriLibUserStringToSingleFilePath(Path, TRUE, &FullPath)) {
        return FALSE;
    }

    FileSize = YoriLibStringToFileSize(SizeAsString);

    StorageType.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_UNKNOWN;
    StorageType.VendorId = VIRTUAL_STORAGE_TYPE_VENDOR_UNKNOWN;

    ZeroMemory(&OpenParams, sizeof(OpenParams));
    OpenParams.Version = OPEN_VIRTUAL_DISK_VERSION_1;
    OpenParams.Version1.RWDepth = OPEN_VIRTUAL_DISK_RW_DEPTH_DEFAULT;

    AccessRequested = VIRTUAL_DISK_ACCESS_METAOPS;

    Err = DllVirtDisk.pOpenVirtualDisk(&StorageType, FullPath.StartOfString, AccessRequested, OPEN_VIRTUAL_DISK_FLAG_NONE, &OpenParams, &Handle);
    if (Err != ERROR_SUCCESS) {
        ErrText = YoriLibGetWinErrorText(Err);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("vhdtool: open of %y failed: %s"), &FullPath, ErrText);
        YoriLibFreeWinErrorText(ErrText);
        YoriLibFreeStringContents(&FullPath);
        return FALSE;
    }

    ExpandParams.Version = 1;
    ExpandParams.Version1.NewSizeInBytes = FileSize.QuadPart;
    Err = DllVirtDisk.pExpandVirtualDisk(Handle, 0, &ExpandParams, NULL);
    if (Err != ERROR_SUCCESS) {
        ErrText = YoriLibGetWinErrorText(Err);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("vhdtool: expand of %y failed: %s"), &FullPath, ErrText);
        YoriLibFreeWinErrorText(ErrText);
        YoriLibFreeStringContents(&FullPath);
        CloseHandle(Handle);
        return FALSE;
    }

    YoriLibFreeStringContents(&FullPath);
    CloseHandle(Handle);
    return TRUE;
}

/**
 Compact a dynamic VHD by removing unused space.

 @param Path Pointer to the path of the file to compact.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
VhdToolCompact(
    __in PYORI_STRING Path
    )
{
    VIRTUAL_STORAGE_TYPE StorageType;
    HANDLE Handle;
    YORI_STRING FullPath;
    OPEN_VIRTUAL_DISK_PARAMETERS OpenParams;
    COMPACT_VIRTUAL_DISK_PARAMETERS CompactParams;
    DWORD Err;
    DWORD AccessRequested;
    LPTSTR ErrText;

    YoriLibLoadVirtDiskFunctions();
    if (DllVirtDisk.pCompactVirtualDisk == NULL ||
        DllVirtDisk.pOpenVirtualDisk == NULL) {

        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("vhdtool: OS support not present\n"));
        return FALSE;
    }

    ZeroMemory(&StorageType, sizeof(StorageType));

    YoriLibInitEmptyString(&FullPath);

    if (!YoriLibUserStringToSingleFilePath(Path, TRUE, &FullPath)) {
        return FALSE;
    }

    StorageType.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_UNKNOWN;
    StorageType.VendorId = VIRTUAL_STORAGE_TYPE_VENDOR_UNKNOWN;

    ZeroMemory(&OpenParams, sizeof(OpenParams));
    OpenParams.Version = OPEN_VIRTUAL_DISK_VERSION_1;
    OpenParams.Version1.RWDepth = OPEN_VIRTUAL_DISK_RW_DEPTH_DEFAULT;

    AccessRequested = VIRTUAL_DISK_ACCESS_METAOPS;

    Err = DllVirtDisk.pOpenVirtualDisk(&StorageType, FullPath.StartOfString, AccessRequested, OPEN_VIRTUAL_DISK_FLAG_NONE, &OpenParams, &Handle);
    if (Err != ERROR_SUCCESS) {
        ErrText = YoriLibGetWinErrorText(Err);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("vhdtool: open of %y failed: %s"), &FullPath, ErrText);
        YoriLibFreeWinErrorText(ErrText);
        YoriLibFreeStringContents(&FullPath);
        return FALSE;
    }

    CompactParams.Version = 1;
    Err = DllVirtDisk.pCompactVirtualDisk(Handle, 0, &CompactParams, NULL);
    if (Err != ERROR_SUCCESS) {
        ErrText = YoriLibGetWinErrorText(Err);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("vhdtool: compact of %y failed: %s"), &FullPath, ErrText);
        YoriLibFreeWinErrorText(ErrText);
        YoriLibFreeStringContents(&FullPath);
        CloseHandle(Handle);
        return FALSE;
    }

    YoriLibFreeStringContents(&FullPath);
    CloseHandle(Handle);
    return TRUE;
}

/**
 Shrink a VHD to a smaller, specified size.

 @param Path Pointer to the path of the file to shrink.

 @param SizeAsString Pointer to a string form of the size of the file to
        shrink to.

 @param ExtType The extension type for the virtual disk.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
VhdToolShrink(
    __in PYORI_STRING Path,
    __in PYORI_STRING SizeAsString,
    __in VHDTOOL_EXT_TYPE ExtType
    )
{
    LARGE_INTEGER FileSize;
    VIRTUAL_STORAGE_TYPE StorageType;
    HANDLE Handle;
    YORI_STRING FullPath;
    OPEN_VIRTUAL_DISK_PARAMETERS OpenParams;
    RESIZE_VIRTUAL_DISK_PARAMETERS ResizeParams;
    DWORD Err;
    DWORD AccessRequested;
    LPTSTR ErrText;

    YoriLibLoadVirtDiskFunctions();
    if (DllVirtDisk.pResizeVirtualDisk == NULL ||
        DllVirtDisk.pOpenVirtualDisk == NULL) {

        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("vhdtool: OS support not present\n"));
        return FALSE;
    }

    if (ExtType != VhdToolExtVhdx &&
        ExtType != VhdToolExtUnknown) {

        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("vhdtool: shrink only supported on vhdx\n"));
        return FALSE;
    }

    ZeroMemory(&StorageType, sizeof(StorageType));

    YoriLibInitEmptyString(&FullPath);

    if (!YoriLibUserStringToSingleFilePath(Path, TRUE, &FullPath)) {
        return FALSE;
    }

    FileSize = YoriLibStringToFileSize(SizeAsString);

    StorageType.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_UNKNOWN;
    StorageType.VendorId = VIRTUAL_STORAGE_TYPE_VENDOR_UNKNOWN;

    ZeroMemory(&OpenParams, sizeof(OpenParams));
    OpenParams.Version = OPEN_VIRTUAL_DISK_VERSION_1;
    OpenParams.Version1.RWDepth = OPEN_VIRTUAL_DISK_RW_DEPTH_DEFAULT;

    AccessRequested = VIRTUAL_DISK_ACCESS_METAOPS | VIRTUAL_DISK_ACCESS_ATTACH_RW;

    Err = DllVirtDisk.pOpenVirtualDisk(&StorageType, FullPath.StartOfString, AccessRequested, OPEN_VIRTUAL_DISK_FLAG_NONE, &OpenParams, &Handle);
    if (Err != ERROR_SUCCESS) {
        ErrText = YoriLibGetWinErrorText(Err);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("vhdtool: open of %y failed: %s"), &FullPath, ErrText);
        YoriLibFreeWinErrorText(ErrText);
        YoriLibFreeStringContents(&FullPath);
        return FALSE;
    }

    ResizeParams.Version = 1;
    ResizeParams.Version1.NewSizeInBytes = FileSize.QuadPart;
    Err = DllVirtDisk.pResizeVirtualDisk(Handle, 1, &ResizeParams, NULL);
    if (Err != ERROR_SUCCESS) {
        ErrText = YoriLibGetWinErrorText(Err);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("vhdtool: shrink of %y failed: %s"), &FullPath, ErrText);
        YoriLibFreeWinErrorText(ErrText);
        YoriLibFreeStringContents(&FullPath);
        CloseHandle(Handle);
        return FALSE;
    }

    YoriLibFreeStringContents(&FullPath);
    CloseHandle(Handle);
    return TRUE;
}

/**
 Merge a differencing VHD with its parent.

 @param Path Pointer to the path of the file to merge.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
VhdToolMerge(
    __in PYORI_STRING Path
    )
{
    VIRTUAL_STORAGE_TYPE StorageType;
    HANDLE Handle;
    YORI_STRING FullPath;
    OPEN_VIRTUAL_DISK_PARAMETERS OpenParams;
    MERGE_VIRTUAL_DISK_PARAMETERS MergeParams;
    DWORD Err;
    DWORD AccessRequested;
    LPTSTR ErrText;

    YoriLibLoadVirtDiskFunctions();
    if (DllVirtDisk.pMergeVirtualDisk == NULL ||
        DllVirtDisk.pOpenVirtualDisk == NULL) {

        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("vhdtool: OS support not present\n"));
        return FALSE;
    }

    ZeroMemory(&StorageType, sizeof(StorageType));

    YoriLibInitEmptyString(&FullPath);

    if (!YoriLibUserStringToSingleFilePath(Path, TRUE, &FullPath)) {
        return FALSE;
    }

    StorageType.DeviceId = VIRTUAL_STORAGE_TYPE_DEVICE_UNKNOWN;
    StorageType.VendorId = VIRTUAL_STORAGE_TYPE_VENDOR_UNKNOWN;

    ZeroMemory(&OpenParams, sizeof(OpenParams));
    OpenParams.Version = OPEN_VIRTUAL_DISK_VERSION_1;
    OpenParams.Version1.RWDepth = 2;

    AccessRequested = VIRTUAL_DISK_ACCESS_METAOPS;

    Err = DllVirtDisk.pOpenVirtualDisk(&StorageType, FullPath.StartOfString, AccessRequested, OPEN_VIRTUAL_DISK_FLAG_NONE, &OpenParams, &Handle);
    if (Err != ERROR_SUCCESS) {
        ErrText = YoriLibGetWinErrorText(Err);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("vhdtool: open of %y failed: %s"), &FullPath, ErrText);
        YoriLibFreeWinErrorText(ErrText);
        YoriLibFreeStringContents(&FullPath);
        return FALSE;
    }

    MergeParams.Version = 1;
    MergeParams.Version1.DepthToMerge = 1;
    Err = DllVirtDisk.pMergeVirtualDisk(Handle, 0, &MergeParams, NULL);
    if (Err != ERROR_SUCCESS) {
        ErrText = YoriLibGetWinErrorText(Err);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("vhdtool: merge of %y failed: %s"), &FullPath, ErrText);
        YoriLibFreeWinErrorText(ErrText);
        YoriLibFreeStringContents(&FullPath);
        CloseHandle(Handle);
        return FALSE;
    }

    YoriLibFreeStringContents(&FullPath);
    CloseHandle(Handle);
    return TRUE;
}

/**
 A set of operations supported by this application.
 */
typedef enum _VHDTOOL_OP {
    VhdToolOpNone = 0,
    VhdToolOpCreateFixedVhd = 1,
    VhdToolOpCreateDynamicVhd = 2,
    VhdToolOpExpand = 3,
    VhdToolOpCompact = 4,
    VhdToolOpShrink = 5,
    VhdToolOpCreateDiffVhd = 6,
    VhdToolOpMerge = 7,
    VhdToolOpCloneFixed = 8,
    VhdToolOpCloneDynamic = 9,
} VHDTOOL_OP;

#ifdef YORI_BUILTIN
/**
 The main entrypoint for the vhdtool builtin command.
 */
#define ENTRYPOINT YoriCmd_VHDTOOL
#else
/**
 The main entrypoint for the vhdtool standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the vhdtool cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the child process on success, or failure if the child
         could not be launched.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg;
    YORI_STRING Arg;
    PYORI_STRING FileName = NULL;
    PYORI_STRING FileParent = NULL;
    PYORI_STRING FileSize = NULL;
    VHDTOOL_OP Op;
    VHDTOOL_EXT_TYPE ExtType;
    VHDTOOL_SECTOR_SIZE SectorSize;

    Op = VhdToolOpNone;
    ExtType = VhdToolExtUnknown;
    SectorSize = VhdToolSectorDefault;

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                VhdToolHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2019"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("clonedynamic")) == 0) {
                if (ArgC > i + 2) {
                    FileName = &ArgV[i + 1];
                    FileParent = &ArgV[i + 2];
                    Op = VhdToolOpCloneDynamic;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("clonefixed")) == 0) {
                if (ArgC > i + 2) {
                    FileName = &ArgV[i + 1];
                    FileParent = &ArgV[i + 2];
                    Op = VhdToolOpCloneFixed;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("compact")) == 0) {
                if (ArgC > i + 1) {
                    FileName = &ArgV[i + 1];
                    Op = VhdToolOpCompact;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("creatediff")) == 0) {
                if (ArgC > i + 2) {
                    FileName = &ArgV[i + 1];
                    FileParent = &ArgV[i + 2];
                    Op = VhdToolOpCreateDiffVhd;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("createdynamic")) == 0) {
                if (ArgC > i + 2) {
                    FileName = &ArgV[i + 1];
                    FileSize = &ArgV[i + 2];
                    Op = VhdToolOpCreateDynamicVhd;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("createfixed")) == 0) {
                if (ArgC > i + 2) {
                    FileName = &ArgV[i + 1];
                    FileSize = &ArgV[i + 2];
                    Op = VhdToolOpCreateFixedVhd;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("expand")) == 0) {
                if (ArgC > i + 2) {
                    FileName = &ArgV[i + 1];
                    FileSize = &ArgV[i + 2];
                    Op = VhdToolOpExpand;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("sector:512")) == 0) {
                SectorSize = VhdToolSector512Native;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("sector:512e")) == 0) {
                SectorSize = VhdToolSector512e;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("sector:4k")) == 0 ||
                       YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("sector:4096")) == 0) {
                SectorSize = VhdToolSector4kNative;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("merge")) == 0) {
                if (ArgC > i + 1) {
                    FileName = &ArgV[i + 1];
                    Op = VhdToolOpMerge;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("shrink")) == 0) {
                if (ArgC > i + 2) {
                    FileName = &ArgV[i + 1];
                    FileSize = &ArgV[i + 2];
                    Op = VhdToolOpShrink;
                    ArgumentUnderstood = TRUE;
                }
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    if (Op == VhdToolOpNone) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("vhdtool: operation not specified\n"));
        return EXIT_FAILURE;
    }

    if (FileName != NULL) {
        YORI_STRING Ext;
        LPTSTR Period;

        Period = YoriLibFindRightMostCharacter(FileName, '.');
        if (Period != NULL) {
            YoriLibInitEmptyString(&Ext);
            Ext.StartOfString = Period + 1;
            Ext.LengthInChars = FileName->LengthInChars - (DWORD)(Period - FileName->StartOfString) - 1;
            if (YoriLibCompareStringWithLiteralInsensitive(&Ext, _T("vhdx")) == 0) {
                ExtType = VhdToolExtVhdx;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Ext, _T("vhd")) == 0) {
                ExtType = VhdToolExtVhd;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Ext, _T("iso")) == 0) {
                ExtType = VhdToolExtIso;
            } 
        }
    }

    switch(Op) {
        case VhdToolOpCloneDynamic:
            if (ExtType == VhdToolExtVhdx) {
                VhdToolCreateNewVhdx(FileName, NULL, FALSE, SectorSize, FileParent);
            } else {
                VhdToolCreateNewVhd(FileName, NULL, FALSE, ExtType, FileParent);
            }
            break;
        case VhdToolOpCloneFixed:
            if (ExtType == VhdToolExtIso) {
                VhdToolCloneIso(FileName, FileParent);
            } else if (ExtType == VhdToolExtVhdx) {
                VhdToolCreateNewVhdx(FileName, NULL, TRUE, SectorSize, FileParent);
            } else {
                VhdToolCreateNewVhd(FileName, NULL, TRUE, ExtType, FileParent);
            }
            break;
        case VhdToolOpCreateFixedVhd:
            if (ExtType == VhdToolExtVhdx) {
                VhdToolCreateNewVhdx(FileName, FileSize, TRUE, SectorSize, NULL);
            } else {
                VhdToolCreateNewVhd(FileName, FileSize, TRUE, ExtType, NULL);
            }
            break;
        case VhdToolOpCreateDynamicVhd:
            if (ExtType == VhdToolExtVhdx) {
                VhdToolCreateNewVhdx(FileName, FileSize, FALSE, SectorSize, NULL);
            } else {
                VhdToolCreateNewVhd(FileName, FileSize, FALSE, ExtType, NULL);
            }
            break;
        case VhdToolOpExpand:
            VhdToolExpand(FileName, FileSize);
            break;
        case VhdToolOpCompact:
            VhdToolCompact(FileName);
            break;
        case VhdToolOpShrink:
            VhdToolShrink(FileName, FileSize, ExtType);
            break;
        case VhdToolOpCreateDiffVhd:
            if (ExtType == VhdToolExtVhdx) {
                VhdToolCreateDiffVhdx(FileName, FileParent);
            } else {
                VhdToolCreateDiffVhd(FileName, FileParent, ExtType);
            }
            break;
        case VhdToolOpMerge:
            VhdToolMerge(FileName);
            break;
    }

    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>libwin/combo.c<|end_filename|>
/**
 * @file libwin/combo.c
 *
 * Yori window combo control
 *
 * Copyright (c) 2019-2020 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "yoripch.h"
#include "yorilib.h"
#include "yoriwin.h"
#include "winpriv.h"

/**
 A structure describing the contents of a combo control.
 */
typedef struct _YORI_WIN_CTRL_COMBO {

    /**
     A common header for all controls
     */
    YORI_WIN_CTRL Ctrl;

    /**
     Pointer to the child edit control that renders the text within the
     combo.
     */
    PYORI_WIN_CTRL Edit;

    /**
     The set of options to display in the drop down list.
     */
    YORI_WIN_ITEM_ARRAY ItemArray;

    /**
     A function to invoke when the item is changed via any mechanism.
     */
    PYORI_WIN_NOTIFY ClickCallback;

    /**
     The index within ItemArray of the array element that is currently
     highlighted
     */
    DWORD ActiveOption;

    /**
     Set to TRUE if any option is activated.  FALSE if no item has been
     activated.
     */
    BOOLEAN ItemActive;

    /**
     The number of lines to display in the pulldown list.
     */
    WORD LinesInList;

} YORI_WIN_CTRL_COMBO, *PYORI_WIN_CTRL_COMBO;

/**
 Returns the currently active option within the combo control.

 @param CtrlHandle Pointer to the combo control.

 @param CurrentlyActiveIndex On successful completion, populated with the
        currently selected combo item.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOLEAN
YoriWinComboGetActiveOption(
    __in PYORI_WIN_CTRL_HANDLE CtrlHandle,
    __out PDWORD CurrentlyActiveIndex
    )
{
    PYORI_WIN_CTRL Ctrl;
    PYORI_WIN_CTRL_COMBO Combo;
    Ctrl = (PYORI_WIN_CTRL)CtrlHandle;
    Combo = CONTAINING_RECORD(Ctrl, YORI_WIN_CTRL_COMBO, Ctrl);
    if (!Combo->ItemActive) {
        return FALSE;
    }
    *CurrentlyActiveIndex = Combo->ActiveOption;
    return TRUE;
}

/**
 Set the currently selected option within the combo control.

 @param CtrlHandle Pointer to the combo control.

 @param ActiveOption Specifies the index of the item to select.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOLEAN
YoriWinComboSetActiveOption(
    __inout PYORI_WIN_CTRL_HANDLE CtrlHandle,
    __in DWORD ActiveOption
    )
{
    PYORI_WIN_CTRL Ctrl;
    PYORI_WIN_CTRL_COMBO Combo;
    Ctrl = (PYORI_WIN_CTRL)CtrlHandle;
    Combo = CONTAINING_RECORD(Ctrl, YORI_WIN_CTRL_COMBO, Ctrl);

    if (ActiveOption < Combo->ItemArray.Count) {
        Combo->ItemActive = TRUE;
        Combo->ActiveOption = ActiveOption;
        YoriWinEditSetText(Combo->Edit, &Combo->ItemArray.Items[Combo->ActiveOption].String);
        return TRUE;
    }

    return FALSE;
}


/**
 A function to be invoked when an event of interest occurs when the list popup
 window is displayed.

 @param Ctrl Pointer to the window control.

 @param Event Pointer to the event information.

 @return TRUE to indicate that the event was processed and no further
         processing should occur.  FALSE to indicate regular processing
         should continue.
 */
BOOLEAN
YoriWinComboChildEvent(
    __in PYORI_WIN_CTRL Ctrl,
    __in PYORI_WIN_EVENT Event
    )
{
    PYORI_WIN_WINDOW Window;
    PYORI_WIN_CTRL ListCtrl;
    DWORD ActiveIndex;

    Window = YoriWinGetWindowFromWindowCtrl(Ctrl);
    ListCtrl = YoriWinFindControlById(Ctrl, 1);
    if (ListCtrl == NULL) {
        return FALSE;
    }

    switch(Event->EventType) {
        case YoriWinEventKeyDown:
            if (Event->KeyDown.VirtualKeyCode == VK_ESCAPE) {
                YoriWinCloseWindow(Window, 0);
            } else if (Event->KeyDown.VirtualKeyCode == VK_RETURN ||
                       Event->KeyDown.VirtualKeyCode == VK_TAB) {
                if (YoriWinListGetActiveOption(ListCtrl, &ActiveIndex)) {
                    YoriWinCloseWindow(Window, ActiveIndex + 1);
                }
            }
            break;
        case YoriWinEventMouseDownInClient:
            if (YoriWinListGetActiveOption(ListCtrl, &ActiveIndex)) {
                YoriWinCloseWindow(Window, ActiveIndex + 1);
            }
            break;
        case YoriWinEventMouseDownOutsideWindow:
            YoriWinCloseWindow(Window, 0);
            break;
    }
    return FALSE;
}

/**
 When the downward arrow is activated, display the pulldown list populated
 with possible items.

 @param Combo Pointer to the combo control.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOLEAN
YoriWinComboDisplayPullDown(
    __in PYORI_WIN_CTRL_COMBO Combo
    )
{
    PYORI_WIN_CTRL Ctrl;
    COORD CtrlCoord;
    COORD ScreenCoord;
    PYORI_WIN_WINDOW TopLevelWindow;
    PYORI_WIN_WINDOW_MANAGER_HANDLE WinMgrHandle;
    PYORI_WIN_WINDOW_HANDLE ComboChildWindow;
    PYORI_WIN_CTRL List;
    SMALL_RECT ChildRect;
    SMALL_RECT ListRect;
    DWORD_PTR ChildResult;

    Ctrl = &Combo->Ctrl;

    CtrlCoord.X = 0;
    CtrlCoord.Y = 0;
    YoriWinTranslateCtrlCoordinatesToScreenCoordinates(Ctrl, FALSE, CtrlCoord, &ScreenCoord);
    TopLevelWindow = YoriWinGetTopLevelWindow(Ctrl);
    WinMgrHandle = YoriWinGetWindowManagerHandle(TopLevelWindow);

    ChildRect.Left = ScreenCoord.X;
    ChildRect.Top = (SHORT)(ScreenCoord.Y + 1);
    ChildRect.Right = (SHORT)(ChildRect.Left + Ctrl->FullRect.Right - Ctrl->FullRect.Left);
    ChildRect.Bottom = (SHORT)(ChildRect.Top + Combo->LinesInList + 1);

    if (!YoriWinCreateWindowEx(WinMgrHandle, &ChildRect, 0, NULL, &ComboChildWindow)) {
        return FALSE;
    }

    ListRect.Left = 0;
    ListRect.Top = 0;
    ListRect.Right = (SHORT)(ChildRect.Right - ChildRect.Left);
    ListRect.Bottom = (SHORT)(ChildRect.Bottom - ChildRect.Top);
    List = YoriWinListCreate(ComboChildWindow, &ListRect, YORI_WIN_LIST_STYLE_VSCROLLBAR);
    if (List == NULL) {
        YoriWinDestroyWindow(ComboChildWindow);
        return FALSE;
    }

    YoriWinSetControlId(List, 1);
    if (Combo->ItemArray.Count > 0) {
        YoriWinListAddItemArray(List, &Combo->ItemArray);
    }

    YoriWinSetCustomNotification(ComboChildWindow, YoriWinEventKeyDown, YoriWinComboChildEvent);
    YoriWinSetCustomNotification(ComboChildWindow, YoriWinEventMouseDownInClient, YoriWinComboChildEvent);
    YoriWinSetCustomNotification(ComboChildWindow, YoriWinEventMouseDownOutsideWindow, YoriWinComboChildEvent);
    ChildResult = 0;
    if (!YoriWinProcessInputForWindow(ComboChildWindow, &ChildResult)) {
        ChildResult = 0;
    }

    YoriWinDestroyWindow(ComboChildWindow);

    //
    //  Don't change the text until the edit control has focus again, so it
    //  knows where to put the cursor.
    //

    if (ChildResult > 0) {
        if (YoriWinComboSetActiveOption(&Combo->Ctrl, (DWORD)(ChildResult - 1))) {
            if (Combo->ClickCallback != NULL) {
                Combo->ClickCallback(&Combo->Ctrl);
            }
        }
    }

    return TRUE;
}

/**
 Process input events for a combo control.

 @param Ctrl Pointer to the combo control.

 @param Event Pointer to the input event.

 @return TRUE to indicate that the event was processed and no further
         processing should occur.  FALSE to indicate that regular processing
         should continue (although this does not imply that no processing
         has already occurred.)
 */
BOOLEAN
YoriWinComboEventHandler(
    __in PYORI_WIN_CTRL Ctrl,
    __in PYORI_WIN_EVENT Event
    )
{
    PYORI_WIN_CTRL_COMBO Combo;
    Combo = CONTAINING_RECORD(Ctrl, YORI_WIN_CTRL_COMBO, Ctrl);
    switch(Event->EventType) {
        case YoriWinEventKeyDown:
            if (Event->KeyDown.VirtualKeyCode == VK_DOWN) {
                YoriWinComboDisplayPullDown(Combo);
            }
            break;
        case YoriWinEventExecute:
            break;
        case YoriWinEventParentDestroyed:
            if (Combo->Edit->NotifyEventFn != NULL) {
                Combo->Edit->NotifyEventFn(Combo->Edit, Event);
            }
            YoriWinItemArrayCleanup(&Combo->ItemArray);
            YoriWinDestroyControl(Ctrl);
            YoriLibDereference(Combo);
            break;
        case YoriWinEventMouseDownInNonClient:
            YoriWinComboDisplayPullDown(Combo);
            break;
        case YoriWinEventMouseDownInClient:
            break;
        case YoriWinEventMouseUpInClient:
        case YoriWinEventMouseUpInNonClient:
            break;
        case YoriWinEventMouseUpOutsideWindow:
            break;
        case YoriWinEventLoseFocus:
        case YoriWinEventGetFocus:
            if (Combo->Edit != NULL &&
                Combo->Edit->NotifyEventFn != NULL) {

                Combo->Edit->NotifyEventFn(Combo->Edit, Event);
            }
            break;
    }

    return FALSE;
}

/**
 Adds new items to the combo control.

 @param CtrlHandle Pointer to the combo control to add items to.

 @param ListOptions Pointer to an array of options to include in the list
        control.

 @param NumberOptions Specifies the number of options in the above array.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOLEAN
YoriWinComboAddItems(
    __in PYORI_WIN_CTRL_HANDLE CtrlHandle,
    __in PYORI_STRING ListOptions,
    __in DWORD NumberOptions
    )
{
    PYORI_WIN_CTRL Ctrl;
    PYORI_WIN_CTRL_COMBO Combo;

    Ctrl = (PYORI_WIN_CTRL)CtrlHandle;
    Combo = CONTAINING_RECORD(Ctrl, YORI_WIN_CTRL_COMBO, Ctrl);

    if (!YoriWinItemArrayAddItems(&Combo->ItemArray, ListOptions, NumberOptions)) {
        return FALSE;
    }

    return TRUE;
}

/**
 Set the size and location of a combo control, and redraw the contents.

 @param CtrlHandle Pointer to the combo to resize or reposition.

 @param CtrlRect Specifies the new size and position of the combo.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOLEAN
YoriWinComboReposition(
    __in PYORI_WIN_CTRL_HANDLE CtrlHandle,
    __in PSMALL_RECT CtrlRect
    )
{
    PYORI_WIN_CTRL Ctrl = (PYORI_WIN_CTRL)CtrlHandle;
    PYORI_WIN_CTRL_COMBO Combo;
    CONST TCHAR* DownChars;
    PYORI_WIN_WINDOW_MANAGER_HANDLE WinMgrHandle;

    Ctrl = (PYORI_WIN_CTRL)CtrlHandle;
    Combo = CONTAINING_RECORD(Ctrl, YORI_WIN_CTRL_COMBO, Ctrl);

    if (!YoriWinControlReposition(Ctrl, CtrlRect)) {
        return FALSE;
    }

    WinMgrHandle = YoriWinGetWindowManagerHandle(YoriWinGetTopLevelWindow(Ctrl));
    DownChars = YoriWinGetDrawingCharacters(WinMgrHandle, YoriWinCharsComboDown);

    YoriWinSetControlNonClientCell(&Combo->Ctrl, (SHORT)(Combo->Ctrl.ClientRect.Right + 1), 0, DownChars[0], Combo->Ctrl.DefaultAttributes);
    YoriWinEditReposition(Combo->Edit, &Combo->Ctrl.ClientRect);
    return TRUE;
}


/**
 Create a combo control and add it to a window.  This is destroyed when the
 window is destroyed.

 @param ParentHandle Pointer to the parent window.

 @param Size Specifies the location and size of the combo.

 @param LinesInList Specifies the number of lines to display in the dropdown
        list.  Note this refers to the number of entries in the list (ie., the
        client area of the list), which is less than the list will use to
        render to the screen due to the border.

 @param Caption Specifies the text to display on the combo.

 @param Style Specifies style flags for the combo including whether it is the
        default or cancel combo on a window.

 @param ClickCallback A function to invoke when the combo is pressed.

 @return Pointer to the newly created control or NULL on failure.
 */
PYORI_WIN_CTRL_HANDLE
YoriWinComboCreate(
    __in PYORI_WIN_WINDOW_HANDLE ParentHandle,
    __in PSMALL_RECT Size,
    __in WORD LinesInList,
    __in PYORI_STRING Caption,
    __in DWORD Style,
    __in_opt PYORI_WIN_NOTIFY ClickCallback
    )
{
    PYORI_WIN_CTRL_COMBO Combo;
    PYORI_WIN_WINDOW Parent;
    CONST TCHAR* DownChars;
    PYORI_WIN_WINDOW_MANAGER_HANDLE WinMgrHandle;

    Parent = (PYORI_WIN_WINDOW)ParentHandle;

    UNREFERENCED_PARAMETER(Style);

    Combo = YoriLibReferencedMalloc(sizeof(YORI_WIN_CTRL_COMBO));
    if (Combo == NULL) {
        return NULL;
    }

    ZeroMemory(Combo, sizeof(YORI_WIN_CTRL_COMBO));

    YoriWinItemArrayInitialize(&Combo->ItemArray);

    Combo->Ctrl.NotifyEventFn = YoriWinComboEventHandler;
    if (!YoriWinCreateControl(Parent, Size, TRUE, &Combo->Ctrl)) {
        YoriLibDereference(Combo);
        return NULL;
    }

    WinMgrHandle = YoriWinGetWindowManagerHandle(YoriWinGetTopLevelWindow(&Combo->Ctrl));
    DownChars = YoriWinGetDrawingCharacters(WinMgrHandle, YoriWinCharsComboDown);


    YoriWinSetControlNonClientCell(&Combo->Ctrl, Combo->Ctrl.ClientRect.Right, 0, DownChars[0], Combo->Ctrl.DefaultAttributes);

    Combo->Ctrl.ClientRect.Right--;

    Combo->Edit = YoriWinEditCreate(&Combo->Ctrl, &Combo->Ctrl.ClientRect, Caption, YORI_WIN_EDIT_STYLE_READ_ONLY);
    if (Combo->Edit == NULL) {
        YoriWinDestroyControl(&Combo->Ctrl);
        YoriLibDereference(Combo);
        return NULL;
    }

    Combo->LinesInList = LinesInList;
    Combo->ClickCallback = ClickCallback;

    return &Combo->Ctrl;
}


// vim:sw=4:ts=4:et:


<|start_filename|>make/alloc.c<|end_filename|>
/**
 * @file make/alloc.c
 *
 * Yori shell make slab allocator
 *
 * Copyright (c) 2020 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>
#include <yorish.h>
#include "make.h"

/**
 A structure before each allocation returned to callers.
 */
typedef struct _MAKE_SLAB_ALLOC_HDR {

    /**
     Pointer to the base of the memory allocation which should be dereferenced
     on free.
     */
    PVOID MemoryToFree;
} MAKE_SLAB_ALLOC_HDR, *PMAKE_SLAB_ALLOC_HDR;

/**
 Allocate a new fixed sized structure.

 @param Alloc Pointer to the allocator which tracks the allocation backing
        multiple fixed sized structures.

 @param SizeInBytes The size of the item to allocate.

 @return Pointer to the newly allocated object or NULL on allocation failure.
 */
PVOID
MakeSlabAlloc(
    __in PMAKE_SLAB_ALLOC Alloc,
    __in DWORD SizeInBytes
    )
{
    PMAKE_SLAB_ALLOC_HDR Hdr;

    if (Alloc->ElementSize != 0 &&
        Alloc->ElementSize != SizeInBytes) {

        return NULL;
    }

    if (Alloc->ElementSize == 0) {
        Alloc->ElementSize = SizeInBytes;
    }

    if (Alloc->NumberAllocatedFromSystem == Alloc->NumberAllocatedToCaller) {
        if (Alloc->Buffer != NULL) {
            YoriLibDereference(Alloc->Buffer);
        }

        Alloc->Buffer = YoriLibReferencedMalloc(0x100 * (sizeof(MAKE_SLAB_ALLOC_HDR) + Alloc->ElementSize));
        if (Alloc->Buffer == NULL) {
            return NULL;
        }

        Alloc->NumberAllocatedFromSystem = 0x100;
        Alloc->NumberAllocatedToCaller = 0;
    }

    Hdr = YoriLibAddToPointer(Alloc->Buffer, Alloc->NumberAllocatedToCaller * (Alloc->ElementSize + sizeof(MAKE_SLAB_ALLOC_HDR)));
    Alloc->NumberAllocatedToCaller++;
    Hdr->MemoryToFree = Alloc->Buffer;
    YoriLibReference(Alloc->Buffer);
    return (Hdr + 1);
}

/**
 Return the header location given a pointer that was handed out from a
 previous call to MakeSlabAlloc.

 @param Ptr The previously allocated element.

 @return Pointer to the header.
 */
PMAKE_SLAB_ALLOC_HDR
MakeSlabHdrFromPtr(
    __in PVOID Ptr
    )
{
    return YoriLibSubtractFromPointer(Ptr, sizeof(MAKE_SLAB_ALLOC_HDR));
}

/**
 Free an element that was previously allocated from MakeSlabAlloc.

 @param Ptr Pointer to the element to free.
 */
VOID
MakeSlabFree(
    __in PVOID Ptr
    )
{
    PMAKE_SLAB_ALLOC_HDR Hdr;
    Hdr = MakeSlabHdrFromPtr(Ptr);
    YoriLibDereference(Hdr->MemoryToFree);
}

/**
 Cleanup a slab allocation structure, freeing any memory that was allocated
 from the system but not yet consumed.

 @param Alloc Pointer to the slab allocator.
 */
VOID
MakeSlabCleanup(
    __in PMAKE_SLAB_ALLOC Alloc
    )
{
    if (Alloc->Buffer != NULL) {
        YoriLibDereference(Alloc->Buffer);
    }

    Alloc->Buffer = NULL;
    Alloc->NumberAllocatedFromSystem = 0;
    Alloc->NumberAllocatedToCaller = 0;
    Alloc->ElementSize = 0;
}


// vim:sw=4:ts=4:et:


<|start_filename|>libsh/exec.c<|end_filename|>
/**
 * @file /libsh/exec.c
 *
 * Yori shell helper routines for executing programs
 *
 * Copyright (c) 2017-2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>
#include <yorish.h>

/**
 Capture the current handles used for stdin/stdout/stderr.

 @param RedirectContext Pointer to the block to capture current state into.
 */
VOID
YoriLibShCaptureRedirectContext(
    __out PYORI_LIBSH_PREVIOUS_REDIRECT_CONTEXT RedirectContext
    )
{
    RedirectContext->ResetInput = FALSE;
    RedirectContext->ResetOutput = FALSE;
    RedirectContext->ResetError = FALSE;
    RedirectContext->StdErrRedirectsToStdOut = FALSE;
    RedirectContext->StdOutRedirectsToStdErr = FALSE;

    //
    //  Always duplicate as noninherited.  If we don't override one of the
    //  existing stdin/stdout/stderr values then the original inheritance
    //  is still in effect.  These are only used to restore our process.
    //

    RedirectContext->StdInput = GetStdHandle(STD_INPUT_HANDLE);
    RedirectContext->StdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    RedirectContext->StdError = GetStdHandle(STD_ERROR_HANDLE);
}

/**
 Revert the redirection context previously put in place by a call to
 @ref YoriLibShInitializeRedirection.

 @param PreviousRedirectContext The context to revert to.
 */
VOID
YoriLibShRevertRedirection(
    __in PYORI_LIBSH_PREVIOUS_REDIRECT_CONTEXT PreviousRedirectContext
    )
{
    YORI_LIBSH_PREVIOUS_REDIRECT_CONTEXT CurrentRedirectContext;

    SetConsoleCtrlHandler(NULL, TRUE);

    YoriLibShCaptureRedirectContext(&CurrentRedirectContext);

    if (PreviousRedirectContext->ResetInput) {
        SetStdHandle(STD_INPUT_HANDLE, PreviousRedirectContext->StdInput);
        CloseHandle(CurrentRedirectContext.StdInput);
    }

    if (PreviousRedirectContext->ResetOutput) {
        SetStdHandle(STD_OUTPUT_HANDLE, PreviousRedirectContext->StdOutput);
        if (!PreviousRedirectContext->StdOutRedirectsToStdErr) {
            CloseHandle(CurrentRedirectContext.StdOutput);
        }
    }

    if (PreviousRedirectContext->ResetError) {
        SetStdHandle(STD_ERROR_HANDLE, PreviousRedirectContext->StdError);
        if (!PreviousRedirectContext->StdErrRedirectsToStdOut) {
            CloseHandle(CurrentRedirectContext.StdError);
        }
    }
}

/**
 Temporarily set this process to have the same stdin/stdout/stderr as a
 a program that it intends to launch.  Keep information about the current
 stdin/stdout/stderr in PreviousRedirectContext so that it can be restored
 with a later call to @ref YoriLibShRevertRedirection.  If any error occurs,
 the Win32 error code is returned and redirection is restored to its
 original state.

 @param ExecContext The context of the program whose stdin/stdout/stderr
        should be initialized.

 @param PrepareForBuiltIn If TRUE, leave Ctrl+C handling suppressed.  The
        builtin program is free to enable it if it has long running
        processing to perform, and it can install its own handler.

 @param PreviousRedirectContext On successful completion, this is populated
        with the current stdin/stdout/stderr information so that it can
        be restored later.

 @return ERROR_SUCCESS to indicate that redirection has been completely
         initialized, or a Win32 error code indicating if an error has
         occurred.
 */
DWORD
YoriLibShInitializeRedirection(
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext,
    __in BOOL PrepareForBuiltIn,
    __out PYORI_LIBSH_PREVIOUS_REDIRECT_CONTEXT PreviousRedirectContext
    )
{
    SECURITY_ATTRIBUTES InheritHandle;
    HANDLE Handle;
    DWORD Error;

    ZeroMemory(&InheritHandle, sizeof(InheritHandle));
    InheritHandle.nLength = sizeof(InheritHandle);
    InheritHandle.bInheritHandle = TRUE;

    YoriLibShCaptureRedirectContext(PreviousRedirectContext);

    //
    //  MSFIX: What this is doing is allowing child processes to see Ctrl+C,
    //  which is wrong, because we only want the foreground process to see it
    //  which implies handling it in the shell.  Unfortunately
    //  GenerateConsoleCtrlEvent has a nasty bug where it can only safely be
    //  called on console processes remaining in this console, which will
    //  require more processing to determine.
    //

    if (!PrepareForBuiltIn) {
        SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT);
        SetConsoleCtrlHandler(NULL, FALSE);
    }

    Error = ERROR_SUCCESS;

    if (ExecContext->StdInType == StdInTypeFile) {
        Handle = CreateFile(ExecContext->StdIn.File.FileName.StartOfString,
                            GENERIC_READ,
                            FILE_SHARE_DELETE | FILE_SHARE_READ,
                            &InheritHandle,
                            OPEN_EXISTING,
                            FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS,
                            NULL);

        if (Handle != INVALID_HANDLE_VALUE) {
            PreviousRedirectContext->ResetInput = TRUE;
            SetStdHandle(STD_INPUT_HANDLE, Handle);
        } else {
            Error = GetLastError();
        }
    } else if (ExecContext->StdInType == StdInTypeNull) {
        Handle = CreateFile(_T("NUL"),
                            GENERIC_READ,
                            FILE_SHARE_DELETE | FILE_SHARE_READ,
                            &InheritHandle,
                            OPEN_EXISTING,
                            FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS,
                            NULL);

        if (Handle != INVALID_HANDLE_VALUE) {
            PreviousRedirectContext->ResetInput = TRUE;
            SetStdHandle(STD_INPUT_HANDLE, Handle);
        } else {
            Error = GetLastError();
        }
    } else if (ExecContext->StdInType == StdInTypePipe) {
        if (ExecContext->StdIn.Pipe.PipeFromPriorProcess != NULL) {

            HANDLE NewHandle;

            if (!YoriLibMakeInheritableHandle(ExecContext->StdIn.Pipe.PipeFromPriorProcess,
                                              &NewHandle)) {

                Error = GetLastError();
            } else {
                PreviousRedirectContext->ResetInput = TRUE;
                SetStdHandle(STD_INPUT_HANDLE, NewHandle);
                ExecContext->StdIn.Pipe.PipeFromPriorProcess = NULL;
            }

        } else {
            HANDLE ReadHandle;
            HANDLE WriteHandle;
            HANDLE NewHandle;

            if (CreatePipe(&ReadHandle, &WriteHandle, NULL, 0)) {
                if (!YoriLibMakeInheritableHandle(ReadHandle, &NewHandle)) {
                    Error = GetLastError();
                    CloseHandle(ReadHandle);
                    CloseHandle(WriteHandle);
                } else {
                    PreviousRedirectContext->ResetInput = TRUE;
                    SetStdHandle(STD_INPUT_HANDLE, NewHandle);
                    CloseHandle(WriteHandle);
                }
            } else {
                Error = GetLastError();
            }
        }
    }

    if (Error != ERROR_SUCCESS) {
        YoriLibShRevertRedirection(PreviousRedirectContext);
        return Error;
    }


    if (ExecContext->StdOutType == StdOutTypeOverwrite) {
        Handle = CreateFile(ExecContext->StdOut.Overwrite.FileName.StartOfString,
                            GENERIC_WRITE,
                            FILE_SHARE_DELETE | FILE_SHARE_READ,
                            &InheritHandle,
                            CREATE_ALWAYS,
                            FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS,
                            NULL);

        if (Handle != INVALID_HANDLE_VALUE) {
            PreviousRedirectContext->ResetOutput = TRUE;
            SetStdHandle(STD_OUTPUT_HANDLE, Handle);
        } else {
            Error = GetLastError();
        }

    } else if (ExecContext->StdOutType == StdOutTypeAppend) {
        Handle = CreateFile(ExecContext->StdOut.Append.FileName.StartOfString,
                            FILE_APPEND_DATA|SYNCHRONIZE,
                            FILE_SHARE_DELETE | FILE_SHARE_READ,
                            &InheritHandle,
                            OPEN_ALWAYS,
                            FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS,
                            NULL);

        if (Handle != INVALID_HANDLE_VALUE) {
            PreviousRedirectContext->ResetOutput = TRUE;
            SetStdHandle(STD_OUTPUT_HANDLE, Handle);
        } else {
            Error = GetLastError();
        }
    } else if (ExecContext->StdOutType == StdOutTypeNull) {
        Handle = CreateFile(_T("NUL"),
                            GENERIC_WRITE,
                            FILE_SHARE_DELETE | FILE_SHARE_READ,
                            &InheritHandle,
                            CREATE_ALWAYS,
                            FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS,
                            NULL);

        if (Handle != INVALID_HANDLE_VALUE) {
            PreviousRedirectContext->ResetOutput = TRUE;
            SetStdHandle(STD_OUTPUT_HANDLE, Handle);
        } else {
            Error = GetLastError();
        }
    } else if (ExecContext->StdOutType == StdOutTypePipe) {
        HANDLE ReadHandle;
        HANDLE WriteHandle;
        HANDLE NewHandle;

        if (ExecContext->NextProgram != NULL &&
            ExecContext->NextProgram->StdInType == StdInTypePipe) {

            if (CreatePipe(&ReadHandle, &WriteHandle, NULL, 0)) {

                if (!YoriLibMakeInheritableHandle(WriteHandle, &NewHandle)) {
                    Error = GetLastError();
                    CloseHandle(ReadHandle);
                    CloseHandle(WriteHandle);
                } else {
                    PreviousRedirectContext->ResetOutput = TRUE;
                    SetStdHandle(STD_OUTPUT_HANDLE, NewHandle);
                    ExecContext->NextProgram->StdIn.Pipe.PipeFromPriorProcess = ReadHandle;
                }
            } else {
                Error = GetLastError();
            }
        }
    } else if (ExecContext->StdOutType == StdOutTypeBuffer) {
        HANDLE ReadHandle;
        HANDLE WriteHandle;
        HANDLE NewHandle;
        if (CreatePipe(&ReadHandle, &WriteHandle, NULL, 0)) {

            if (!YoriLibMakeInheritableHandle(WriteHandle, &NewHandle)) {
                Error = GetLastError();
                CloseHandle(ReadHandle);
                CloseHandle(WriteHandle);
            } else {
                PreviousRedirectContext->ResetOutput = TRUE;
                SetStdHandle(STD_OUTPUT_HANDLE, NewHandle);
                ExecContext->StdOut.Buffer.PipeFromProcess = ReadHandle;
            }
        } else {
            Error = GetLastError();
        }
    }

    if (Error != ERROR_SUCCESS) {
        YoriLibShRevertRedirection(PreviousRedirectContext);
        return Error;
    }

    if (ExecContext->StdErrType == StdErrTypeOverwrite) {
        Handle = CreateFile(ExecContext->StdErr.Overwrite.FileName.StartOfString,
                            GENERIC_WRITE,
                            FILE_SHARE_DELETE | FILE_SHARE_READ,
                            &InheritHandle,
                            CREATE_ALWAYS,
                            FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS,
                            NULL);

        if (Handle != INVALID_HANDLE_VALUE) {
            PreviousRedirectContext->ResetError = TRUE;
            SetStdHandle(STD_ERROR_HANDLE, Handle);
        } else {
            Error = GetLastError();
        }

    } else if (ExecContext->StdErrType == StdErrTypeAppend) {
        Handle = CreateFile(ExecContext->StdErr.Append.FileName.StartOfString,
                            FILE_APPEND_DATA|SYNCHRONIZE,
                            FILE_SHARE_DELETE | FILE_SHARE_READ,
                            &InheritHandle,
                            OPEN_ALWAYS,
                            FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS,
                            NULL);

        if (Handle != INVALID_HANDLE_VALUE) {
            PreviousRedirectContext->ResetError = TRUE;
            SetStdHandle(STD_ERROR_HANDLE, Handle);
        } else {
            Error = GetLastError();
        }
    } else if (ExecContext->StdErrType == StdErrTypeNull) {
        Handle = CreateFile(_T("NUL"),
                            GENERIC_WRITE,
                            FILE_SHARE_DELETE | FILE_SHARE_READ,
                            &InheritHandle,
                            CREATE_ALWAYS,
                            FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS,
                            NULL);

        if (Handle != INVALID_HANDLE_VALUE) {
            PreviousRedirectContext->ResetError = TRUE;
            SetStdHandle(STD_ERROR_HANDLE, Handle);
        } else {
            Error = GetLastError();
        }
    } else if (ExecContext->StdErrType == StdErrTypeBuffer) {
        HANDLE ReadHandle;
        HANDLE WriteHandle;
        HANDLE NewHandle;
        if (CreatePipe(&ReadHandle, &WriteHandle, NULL, 0)) {

            if (!YoriLibMakeInheritableHandle(WriteHandle, &NewHandle)) {
                Error = GetLastError();
                CloseHandle(ReadHandle);
                CloseHandle(WriteHandle);
            } else {
                PreviousRedirectContext->ResetError = TRUE;
                SetStdHandle(STD_ERROR_HANDLE, NewHandle);
                ExecContext->StdErr.Buffer.PipeFromProcess = ReadHandle;
            }
        } else {
            Error = GetLastError();
        }
    }

    if (Error != ERROR_SUCCESS) {
        YoriLibShRevertRedirection(PreviousRedirectContext);
        return Error;
    }

    if (ExecContext->StdErrType == StdErrTypeStdOut) {
        ASSERT(ExecContext->StdOutType != StdOutTypeStdErr);
        PreviousRedirectContext->ResetError = TRUE;
        PreviousRedirectContext->StdErrRedirectsToStdOut = TRUE;
        SetStdHandle(STD_ERROR_HANDLE, GetStdHandle(STD_OUTPUT_HANDLE));
    } else if (ExecContext->StdOutType == StdOutTypeStdErr) {
        PreviousRedirectContext->ResetOutput = TRUE;
        PreviousRedirectContext->StdOutRedirectsToStdErr = TRUE;
        SetStdHandle(STD_OUTPUT_HANDLE, GetStdHandle(STD_ERROR_HANDLE));
    }

    return ERROR_SUCCESS;
}

/**
 Given a process that has been launched and is currently suspended, examine
 its in memory state to determine which subsystem the process will operate in.

 @param ProcessHandle Specifies the executing process.

 @param Subsystem On successful completion, indicates the subsystem that was
        marked in the executable's PE header.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOLEAN
YoriLibShGetSubsystemFromExecutingImage(
    __in HANDLE ProcessHandle,
    __out PWORD Subsystem
    )
{
    PROCESS_BASIC_INFORMATION BasicInfo;

    LONG Status;
    DWORD dwBytesReturned;
    SIZE_T BytesReturned;
    PVOID ImageBaseAddress;
    BOOL TargetProcess32BitPeb;
    PVOID PeHeaderPtr;

    //
    //  Since only one is read at a time, use the same buffer for all.
    //
    union {
        YORI_LIB_PEB32_NATIVE ProcessPeb32;
        YORI_LIB_PEB64 ProcessPeb64;
        IMAGE_DOS_HEADER DosHeader;
        YORILIB_PE_HEADERS PeHeaders;
    } u;

    if (DllNtDll.pNtQueryInformationProcess == NULL) {
        return FALSE;
    }

    TargetProcess32BitPeb = YoriLibDoesProcessHave32BitPeb(ProcessHandle);

    Status = DllNtDll.pNtQueryInformationProcess(ProcessHandle, 0, &BasicInfo, sizeof(BasicInfo), &dwBytesReturned);
    if (Status != 0) {
        return FALSE;
    }

    if (TargetProcess32BitPeb) {
        if (!ReadProcessMemory(ProcessHandle, BasicInfo.PebBaseAddress, &u.ProcessPeb32, sizeof(u.ProcessPeb32), &BytesReturned)) {
            return FALSE;
        }

        ImageBaseAddress = (PVOID)(ULONG_PTR)u.ProcessPeb32.ImageBaseAddress;
    } else {

        if (!ReadProcessMemory(ProcessHandle, BasicInfo.PebBaseAddress, &u.ProcessPeb64, sizeof(u.ProcessPeb64), &BytesReturned)) {
            return FALSE;
        }

        ImageBaseAddress = (PVOID)(ULONG_PTR)u.ProcessPeb64.ImageBaseAddress;
    }

    if (!ReadProcessMemory(ProcessHandle, ImageBaseAddress, &u.DosHeader, sizeof(u.DosHeader), &BytesReturned)) {
        return FALSE;
    }

    if (BytesReturned < sizeof(u.DosHeader) ||
        u.DosHeader.e_magic != IMAGE_DOS_SIGNATURE ||
        u.DosHeader.e_lfanew == 0) {

        return FALSE;
    }

    PeHeaderPtr = YoriLibAddToPointer(ImageBaseAddress, u.DosHeader.e_lfanew);

    if (!ReadProcessMemory(ProcessHandle, PeHeaderPtr, &u.PeHeaders, sizeof(u.PeHeaders), &BytesReturned)) {
        return FALSE;
    }

    if (BytesReturned >= sizeof(YORILIB_PE_HEADERS) &&
        u.PeHeaders.Signature == IMAGE_NT_SIGNATURE &&
        u.PeHeaders.ImageHeader.SizeOfOptionalHeader >= FIELD_OFFSET(IMAGE_OPTIONAL_HEADER, Subsystem) + sizeof(WORD)) {

        *Subsystem = u.PeHeaders.OptionalHeader.Subsystem;
        return TRUE;
    }

    return FALSE;
}

/**
 A wrapper around CreateProcess that sets up redirection and launches a
 process.  The point of this function is that it can be called from the
 main thread or from a debugging thread.

 @param ExecContext Pointer to the ExecContext to attempt to launch via
        CreateProcess.

 @param CurrentDirectory Optionally points to a NULL terminated string
        indicating the current directory to apply.  If NULL, the process
        current directory is used instead.

 @param FailedInRedirection Optionally points to a boolean value to be set to
        TRUE if any error originated while setting up redirection, and FALSE
        if it came from launching the process.

 @return Win32 error code, meaning zero indicates success.
 */
DWORD
YoriLibShCreateProcess(
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext,
    __in_opt LPTSTR CurrentDirectory,
    __out_opt PBOOL FailedInRedirection
    )
{
    YORI_STRING CmdLine;
    PROCESS_INFORMATION ProcessInfo;
    STARTUPINFO StartupInfo;
    YORI_LIBSH_PREVIOUS_REDIRECT_CONTEXT PreviousRedirectContext;
    DWORD CreationFlags = 0;
    DWORD LastError;

    ZeroMemory(&ProcessInfo, sizeof(ProcessInfo));
    if (FailedInRedirection != NULL) {
        *FailedInRedirection = FALSE;
    }

    YoriLibInitEmptyString(&CmdLine);
    if (!YoriLibShBuildCmdlineFromCmdContext(&ExecContext->CmdToExec, &CmdLine, !ExecContext->IncludeEscapesAsLiteral, NULL, NULL)) {
        return ERROR_OUTOFMEMORY;
    }
    ASSERT(YoriLibIsStringNullTerminated(&CmdLine));

    memset(&StartupInfo, 0, sizeof(StartupInfo));
    StartupInfo.cb = sizeof(StartupInfo);

    if (ExecContext->RunOnSecondConsole) {
        CreationFlags |= CREATE_NEW_CONSOLE;
    }

    if (ExecContext->CaptureEnvironmentOnExit) {
        CreationFlags |= DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS;
    }

    CreationFlags |= CREATE_NEW_PROCESS_GROUP | CREATE_DEFAULT_ERROR_MODE | CREATE_SUSPENDED;

    LastError = YoriLibShInitializeRedirection(ExecContext, FALSE, &PreviousRedirectContext);
    if (LastError != ERROR_SUCCESS) {
        YoriLibFreeStringContents(&CmdLine);
        if (FailedInRedirection != NULL) {
            *FailedInRedirection = TRUE;
        }
        return LastError;
    }

    if (!CreateProcess(NULL, CmdLine.StartOfString, NULL, NULL, TRUE, CreationFlags, NULL, CurrentDirectory, &StartupInfo, &ProcessInfo)) {
        LastError = GetLastError();
        YoriLibShRevertRedirection(&PreviousRedirectContext);
        YoriLibFreeStringContents(&CmdLine);
        return LastError;
    } else {
        YoriLibShRevertRedirection(&PreviousRedirectContext);
    }

    //
    //  The nice way to terminate console processes is via
    //  GenerateConsoleCtrlEvent, which gives the child a chance to exit
    //  gracefully.  Unfortunately the console misbehaves very badly if this
    //  is performed on a process that is not a console process.  Default to
    //  non-graceful termination, and see if we can upgrade to graceful
    //  termination by verifying that the process is a console process.
    //

    ExecContext->TerminateGracefully = FALSE;
    if (!ExecContext->RunOnSecondConsole) {
        WORD Subsystem;
        if (YoriLibShGetSubsystemFromExecutingImage(ProcessInfo.hProcess, &Subsystem) &&
            Subsystem == IMAGE_SUBSYSTEM_WINDOWS_CUI) {

            ExecContext->TerminateGracefully = TRUE;
        }
    }

    ResumeThread(ProcessInfo.hThread);

    ASSERT(ExecContext->hProcess == NULL);
    ExecContext->hProcess = ProcessInfo.hProcess;
    ExecContext->hPrimaryThread = ProcessInfo.hThread;
    ExecContext->dwProcessId = ProcessInfo.dwProcessId;

    YoriLibFreeStringContents(&CmdLine);

    return ERROR_SUCCESS;
}

/**
 Cleanup the ExecContext if the process failed to launch.

 @param ExecContext Pointer to the ExecContext to clean up.
 */
VOID
YoriLibShCleanupFailedProcessLaunch(
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext
    )
{
    if (ExecContext->StdOutType == StdOutTypePipe &&
        ExecContext->NextProgram != NULL &&
        ExecContext->NextProgram->StdInType == StdInTypePipe) {

        CloseHandle(ExecContext->NextProgram->StdIn.Pipe.PipeFromPriorProcess);
        ExecContext->NextProgram->StdIn.Pipe.PipeFromPriorProcess = NULL;
        ExecContext->NextProgramType = NextProgramExecNever;
    }
}

/**
 Start buffering process output if the process is configured for it.

 @param ExecContext Pointer to the ExecContext to set up buffers for.
 */
VOID
YoriLibShCommenceProcessBuffersIfNeeded(
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext
    )
{
    //
    //  If we're buffering output, start that process now.  If it succeeds,
    //  the pipe is owned by the buffer pump and shouldn't be torn down
    //  when the ExecContext is.
    //

    if (ExecContext->StdOutType == StdOutTypeBuffer ||
        ExecContext->StdErrType == StdErrTypeBuffer) {

        if (ExecContext->StdOut.Buffer.ProcessBuffers != NULL) {
            ASSERT(ExecContext->StdErrType != StdErrTypeBuffer);
            if (YoriLibShAppendToExistingProcessBuffer(ExecContext)) {
                ExecContext->StdOut.Buffer.PipeFromProcess = NULL;
            } else {
                ExecContext->StdOut.Buffer.ProcessBuffers = NULL;
            }
        } else {
            if (YoriLibShCreateNewProcessBuffer(ExecContext)) {
                if (ExecContext->StdOutType == StdOutTypeBuffer) {
                    ExecContext->StdOut.Buffer.PipeFromProcess = NULL;
                }
                if (ExecContext->StdErrType == StdErrTypeBuffer) {
                    ExecContext->StdErr.Buffer.PipeFromProcess = NULL;
                }
            }
        }
    }
}

/**
 Construct a CmdContext that contains "cmd", "/c" and an arbitrary string.
 CMD uses this syntax to allow a whole pile of things, including redirects,
 to be encoded into a single argument.  On success, the caller is expected
 to free this with @ref YoriLibShFreeCmdContext .

 @param CmdContext Pointer to a CmdContext to be populated within this
        routine.

 @param CmdLine Pointer to an arbitrary command to include.  This is cloned
        (not copied) within this routine, so the caller is expected to not
        modify it or copy it if necessary.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOLEAN
YoriLibShBuildCmdContextForCmdBuckPass (
    __out PYORI_LIBSH_CMD_CONTEXT CmdContext,
    __in PYORI_STRING CmdLine
    )
{
    YORI_STRING Arg;
    YORI_STRING FoundInPath;

    //
    //  Allocate three components, for "cmd", "/c" and "CmdLine"
    //

    CmdContext->ArgC = 3;
    CmdContext->MemoryToFree = YoriLibReferencedMalloc(CmdContext->ArgC * (sizeof(YORI_STRING) + sizeof(YORI_LIBSH_ARG_CONTEXT)));
    if (CmdContext->MemoryToFree == NULL) {
        return FALSE;
    }

    CmdContext->ArgV = CmdContext->MemoryToFree;

    CmdContext->ArgContexts = (PYORI_LIBSH_ARG_CONTEXT)YoriLibAddToPointer(CmdContext->ArgV, sizeof(YORI_STRING) * CmdContext->ArgC);
    ZeroMemory(CmdContext->ArgContexts, sizeof(YORI_LIBSH_ARG_CONTEXT) * CmdContext->ArgC);

    //
    //  Locate "cmd" in PATH
    //

    YoriLibConstantString(&Arg, _T("cmd"));
    YoriLibInitEmptyString(&FoundInPath);
    if (YoriLibLocateExecutableInPath(&Arg, NULL, NULL, &FoundInPath) && FoundInPath.LengthInChars > 0) {
        memcpy(&CmdContext->ArgV[0], &FoundInPath, sizeof(YORI_STRING));
        ASSERT(YoriLibIsStringNullTerminated(&CmdContext->ArgV[0]));
        YoriLibInitEmptyString(&FoundInPath);
    } else {
        YoriLibCloneString(&CmdContext->ArgV[0], &Arg);
    }

    YoriLibFreeStringContents(&FoundInPath);

    YoriLibConstantString(&CmdContext->ArgV[1], _T("/c"));

    //
    //  Add user arg and enforce that it is always quoted.  Note this isn't
    //  a deep copy, just a clone; the caller is expected to copy if needed.
    //

    YoriLibCloneString(&CmdContext->ArgV[2], CmdLine);
    CmdContext->ArgContexts[2].Quoted = TRUE;
    CmdContext->ArgContexts[2].QuoteTerminated = TRUE;

    //
    //  Initialize unused fields
    //

    CmdContext->CurrentArg = 0;
    CmdContext->CurrentArgOffset = 0;
    CmdContext->TrailingChars = FALSE;

    YoriLibShCheckIfArgNeedsQuotes(CmdContext, 0);

    return TRUE;
}

// vim:sw=4:ts=4:et:


<|start_filename|>ypm/rsource.c<|end_filename|>
/**
 * @file ypm/rsource.c
 *
 * Yori shell package manager manage remote sources
 *
 * Copyright (c) 2018-2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>
#include <yoripkg.h>
#include "ypm.h"

/**
 Help text to display to the user.
 */
const
CHAR strYpmRemoteSourceAppendHelpText[] =
        "\n"
        "Append a new remote source to the list to obtain packages from.\n"
        "This source will be consulted after all previous sources.\n"
        "\n"
        "YPM [-license]\n"
        "YPM -rsa <server>\n"
        "\n"
        "   <server>        Specifies a URL that be used to obtain remote packages from\n";

/**
 Help text to display to the user.
 */
const
CHAR strYpmRemoteSourceInsertHelpText[] =
        "\n"
        "Insert a new remote source to the list to obtain packages from.\n"
        "This source will be consulted before all previous sources.\n"
        "\n"
        "YPM [-license]\n"
        "YPM -rsi <server>\n"
        "\n"
        "   <server>        Specifies a URL that be used to obtain remote packages from\n";

/**
 Help text to display to the user.
 */
const
CHAR strYpmRemoteSourceDeleteHelpText[] =
        "\n"
        "Delete a remote source from the list to obtain packages from.\n"
        "\n"
        "YPM [-license]\n"
        "YPM -rsd <server>\n"
        "\n"
        "   <server>        Specifies a URL that should no longer be used to obtain\n"
        "                   remote packages from\n";

/**
 Help text to display to the user.
 */
const
CHAR strYpmRemoteSourceListHelpText[] =
        "\n"
        "List remote sources to obtain packages from.\n"
        "\n"
        "YPM [-license]\n"
        "YPM -rsl\n";

/**
 Display usage text to the user.
 */
BOOL
YpmRemoteSourceAppendHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Ypm %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strYpmRemoteSourceAppendHelpText);
    return TRUE;
}

/**
 Display usage text to the user.
 */
BOOL
YpmRemoteSourceInsertHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Ypm %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strYpmRemoteSourceInsertHelpText);
    return TRUE;
}

/**
 Display usage text to the user.
 */
BOOL
YpmRemoteSourceDeleteHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Ypm %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strYpmRemoteSourceDeleteHelpText);
    return TRUE;
}

/**
 Display usage text to the user.
 */
BOOL
YpmRemoteSourceListHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Ypm %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strYpmRemoteSourceListHelpText);
    return TRUE;
}

/**
 Append a new remote source to the end of the list to obtain packages from.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process.
 */
DWORD
YpmRemoteSourceAppend(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    YORI_STRING Arg;
    PYORI_STRING SourcePath = NULL;

    if (ArgC < 2) {
        YpmRemoteSourceAppendHelp();
        return EXIT_FAILURE;
    }

    SourcePath = &ArgV[1];

    for (i = 2; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                YpmRemoteSourceAppendHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2017-2021"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                ArgumentUnderstood = TRUE;
                StartArg = i + 1;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    YoriPkgAddNewSource(SourcePath, FALSE);

    return EXIT_SUCCESS;
}

/**
 Insert a new remote source to the beginning of the list to obtain packages from.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process.
 */
DWORD
YpmRemoteSourceInsert(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    YORI_STRING Arg;
    PYORI_STRING SourcePath = NULL;

    if (ArgC < 2) {
        YpmRemoteSourceInsertHelp();
        return EXIT_FAILURE;
    }

    SourcePath = &ArgV[1];

    for (i = 2; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                YpmRemoteSourceInsertHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2017-2021"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                ArgumentUnderstood = TRUE;
                StartArg = i + 1;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    YoriPkgAddNewSource(SourcePath, TRUE);

    return EXIT_SUCCESS;
}

/**
 Deletes a remote source from the list to obtain packages from.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process.
 */
DWORD
YpmRemoteSourceDelete(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    YORI_STRING Arg;
    PYORI_STRING SourcePath = NULL;

    if (ArgC < 2) {
        YpmRemoteSourceDeleteHelp();
        return EXIT_FAILURE;
    }

    SourcePath = &ArgV[1];

    for (i = 2; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                YpmRemoteSourceDeleteHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2017-2021"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                ArgumentUnderstood = TRUE;
                StartArg = i + 1;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    YoriPkgDeleteSource(SourcePath);

    return EXIT_SUCCESS;
}

/**
 Lists the currently registered sources to obtain packages from.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process.
 */
DWORD
YpmRemoteSourceList(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    YORI_STRING Arg;

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                YpmRemoteSourceListHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2017-2021"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                ArgumentUnderstood = TRUE;
                StartArg = i + 1;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    YoriPkgDisplaySources();

    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>lib/env.c<|end_filename|>
/**
 * @file lib/env.c
 *
 * Yori lib environment variable control
 *
 * Copyright (c) 2017 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 An implementation of GetEnvironmentStrings that can do appropriate dances
 to work with versions of Windows supporting free, and versions that don't
 support free, as well as those with a W suffix and those without.

 @param EnvStrings On successful completion, populated with the array of
        environment strings.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOL
YoriLibGetEnvironmentStrings(
    __out PYORI_STRING EnvStrings
    )
{
    LPTSTR OsEnvStrings;
    LPSTR OsEnvStringsA;
    DWORD CharCount;

    //
    //  Use GetEnvironmentStringsW where it exists.  Where it doesn't
    //  exist (NT 3.1) we need to upconvert to Unicode.
    //

    if (DllKernel32.pGetEnvironmentStringsW != NULL) {
        OsEnvStrings = DllKernel32.pGetEnvironmentStringsW();
        for (CharCount = 0; OsEnvStrings[CharCount] != '\0' || OsEnvStrings[CharCount + 1] != '\0'; CharCount++);

        CharCount += 2;
        if (!YoriLibAllocateString(EnvStrings, CharCount)) {
            if (DllKernel32.pFreeEnvironmentStringsW) {
                DllKernel32.pFreeEnvironmentStringsW(OsEnvStrings);
            }
            return FALSE;
        }

        memcpy(EnvStrings->StartOfString, OsEnvStrings, CharCount * sizeof(TCHAR));
        EnvStrings->LengthInChars = CharCount;

        if (DllKernel32.pFreeEnvironmentStringsW) {
            DllKernel32.pFreeEnvironmentStringsW(OsEnvStrings);
        }

        return TRUE;
    }

    if (DllKernel32.pGetEnvironmentStrings == NULL) {
        return FALSE;
    }

    OsEnvStringsA = DllKernel32.pGetEnvironmentStrings();

    for (CharCount = 0; OsEnvStringsA[CharCount] != '\0' || OsEnvStringsA[CharCount + 1] != '\0'; CharCount++);

    CharCount += 2;
    if (!YoriLibAllocateString(EnvStrings, CharCount)) {
        return FALSE;
    }
    MultiByteToWideChar(CP_ACP, 0, OsEnvStringsA, CharCount, EnvStrings->StartOfString, CharCount);
    EnvStrings->LengthInChars = CharCount;

    return TRUE;
}

/**
 Returns TRUE if a set of environment strings are double NULL terminated within
 the bounds of the allocation, and populates LengthInChars to encompass the
 double terminator.  If no double terminator is found within the allocation,
 returns FALSE.

 @param EnvStrings Pointer to the environment strings to validate.

 @return TRUE to indicate the environment strings are valid, FALSE if they are
         not.
 */
__success(return)
BOOL
YoriLibAreEnvironmentStringsValid(
    __inout PYORI_STRING EnvStrings
    )
{
    DWORD Index;

    if (EnvStrings->LengthAllocated < 2) {
        return FALSE;
    }

    for (Index = 0; Index < EnvStrings->LengthAllocated - 1; Index++) {
        if (EnvStrings->StartOfString[Index] == '\0' &&
            EnvStrings->StartOfString[Index + 1] == '\0') {

            EnvStrings->LengthInChars = Index + 1;

            return TRUE;
        }
    }

    return FALSE;
}

/**
 Checks if ANSI environment strings are double NULL terminated within
 the bounds of the allocation.  If so, allocates a new Yori string to describe
 the Unicode form of the environment block and populates it with the correct
 length of the buffer.

 @param AnsiEnvStringBuffer Pointer to the environment strings to validate.

 @param BufferLength Number of bytes in the environment string allocation.

 @param UnicodeStrings On successful completion, updated to point to a newly
        allocated set of Unicode strings for the block.

 @return TRUE to indicate the environment strings are valid and could be
         converted to Unicode, FALSE if invalid or not convertible.
 */
__success(return)
BOOL
YoriLibAreAnsiEnvironmentStringsValid(
    __in PUCHAR AnsiEnvStringBuffer,
    __in DWORD BufferLength,
    __out PYORI_STRING UnicodeStrings
    )
{
    DWORD Index;

    YoriLibInitEmptyString(UnicodeStrings);

    if (BufferLength < 2) {
        return FALSE;
    }

    for (Index = 0; Index < BufferLength - 1; Index++) {
        if (AnsiEnvStringBuffer[Index] == '\0' &&
            AnsiEnvStringBuffer[Index + 1] == '\0') {

            if (!YoriLibAllocateString(UnicodeStrings, Index + 2)) {
                return FALSE;
            }

            MultiByteToWideChar(CP_ACP, 0, (LPSTR)AnsiEnvStringBuffer, Index + 2, UnicodeStrings->StartOfString, Index + 2);

            UnicodeStrings->LengthInChars = Index + 2;

            return TRUE;
        }
    }

    return FALSE;
}

/**
 Capture the value from an environment variable, allocating a Yori string of
 appropriate size to contain the contents.

 @param Name Pointer to the name of the variable to obtain.

 @param Value On successful completion, populated with a newly allocated
        string containing the environment variable's contents.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOL
YoriLibAllocateAndGetEnvironmentVariable(
    __in LPCTSTR Name,
    __inout PYORI_STRING Value
    )
{
    DWORD LengthNeeded;

    LengthNeeded = GetEnvironmentVariable(Name, NULL, 0);
    if (LengthNeeded == 0) {
        YoriLibInitEmptyString(Value);
        return TRUE;
    }

    if (LengthNeeded > Value->LengthAllocated) {
        if (!YoriLibReallocateString(Value, LengthNeeded)) {
            return FALSE;
        }
    }

    Value->LengthInChars = GetEnvironmentVariable(Name, Value->StartOfString, Value->LengthAllocated);
    if (Value->LengthInChars == 0 ||
        Value->LengthInChars >= Value->LengthAllocated) {

        YoriLibFreeStringContents(Value);
        return FALSE;
    }

    return TRUE;
}

/**
 Capture the value from an environment variable, convert it to a number and
 return the result.

 @param Name Pointer to the name of the variable to obtain.

 @param Value On successful completion, populated with the contents of the
        variable in numeric form.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOL
YoriLibGetEnvironmentVariableAsNumber(
    __in LPCTSTR Name,
    __out PLONGLONG Value
    )
{
    DWORD LengthNeeded;
    YORI_STRING Temp;
    TCHAR Buffer[32];
    DWORD CharsConsumed;

    LengthNeeded = GetEnvironmentVariable(Name, NULL, 0);

    if (LengthNeeded == 0) {
        return FALSE;
    }

    if (LengthNeeded < sizeof(Buffer)/sizeof(Buffer[0])) {
        YoriLibInitEmptyString(&Temp);
        Temp.StartOfString = Buffer;
        Temp.LengthAllocated = sizeof(Buffer)/sizeof(Buffer[0]);
    } else if (!YoriLibAllocateString(&Temp, LengthNeeded)) {
        return FALSE;
    }

    Temp.LengthInChars = GetEnvironmentVariable(Name, Temp.StartOfString, Temp.LengthAllocated);
    if (Temp.LengthInChars == 0 ||
        Temp.LengthInChars >= Temp.LengthAllocated) {

        YoriLibFreeStringContents(&Temp);
        return FALSE;
    }

    if (!YoriLibStringToNumber(&Temp, FALSE, Value, &CharsConsumed)) {
        return FALSE;
    }

    YoriLibFreeStringContents(&Temp);

    if (CharsConsumed == 0) {
        return FALSE;
    }

    return TRUE;
}

/**
 Add a a new component to a semicolon delimited string.  This routine assumes
 the caller has allocated enough space in ExistingString to hold the result.
 That is, ExistingString must be large enough to hold itself, plus a seperator,
 plus NewComponent, plus a NULL terminator.

 @param ExistingString Pointer to the existing string.  On successful
        completion, this is modified to contain the new string.

 @param NewComponent Pointer to the component to add to the existing string if
        it is not already there.

 @param InsertAtFront If TRUE, the new component is added before existing
        contents; if FALSE, it is added after existing contents.

 @return TRUE if the buffer was modified, FALSE if it was not.
 */
__success(return)
BOOL
YoriLibAddEnvironmentComponentToString(
    __inout PYORI_STRING ExistingString,
    __in PCYORI_STRING NewComponent,
    __in BOOL InsertAtFront
    )
{
    LPTSTR ThisPath;
    TCHAR * TokCtx;

    ASSERT(ExistingString->LengthAllocated >= ExistingString->LengthInChars + 1 + NewComponent->LengthInChars + 1);

    if (ExistingString->LengthAllocated < ExistingString->LengthInChars + 1 + NewComponent->LengthInChars + 1) {
        return FALSE;
    }

    ThisPath = _tcstok_s(ExistingString->StartOfString, _T(";"), &TokCtx);
    while (ThisPath != NULL) {
        if (ThisPath[0] != '\0') {
            if (YoriLibCompareStringWithLiteralInsensitive(NewComponent, ThisPath) == 0) {
                if (TokCtx != NULL) {
                    TokCtx--;
                    ASSERT(*TokCtx == '\0');
                    *TokCtx = ';';
                }
                return FALSE;
            }
        }
        if (TokCtx != NULL) {
            TokCtx--;
            ASSERT(*TokCtx == '\0');
            *TokCtx = ';';
            TokCtx++;
        }
        ThisPath = _tcstok_s(NULL, _T(";"), &TokCtx);
    }

    //
    //  If it currently ends in a semicolon, back up one char
    //  so we don't add another
    //

    if (ExistingString->LengthInChars > 0 &&
        ExistingString->StartOfString[ExistingString->LengthInChars - 1] == ';') {
        ExistingString->LengthInChars--;
    }

    if (InsertAtFront) {

        //
        //  Move the existing contents back to after the new string plus a
        //  seperator, if any existing contents exist.  After inserting the
        //  new string, either add the seperator or terminate, depending
        //  on whether there were contents previously.
        //

        if (ExistingString->LengthInChars > 0) {
            memmove(&ExistingString->StartOfString[NewComponent->LengthInChars + 1],
                    ExistingString->StartOfString,
                    ExistingString->LengthInChars * sizeof(TCHAR));
            ExistingString->StartOfString[NewComponent->LengthInChars + 1 + ExistingString->LengthInChars] = '\0';
        }
        memcpy(ExistingString->StartOfString, NewComponent->StartOfString, NewComponent->LengthInChars * sizeof(TCHAR));
        if (ExistingString->LengthInChars > 0) {
            ExistingString->StartOfString[NewComponent->LengthInChars] = ';';
            ExistingString->LengthInChars += NewComponent->LengthInChars + 1;
        } else {
            ExistingString->StartOfString[NewComponent->LengthInChars] = '\0';
            ExistingString->LengthInChars = NewComponent->LengthInChars;
        }
    } else {

        //
        //  Copy the new path at the end of the previous one.  If it has any
        //  contents, add a seperator.
        //

        if (ExistingString->LengthInChars > 0) {
            ExistingString->StartOfString[ExistingString->LengthInChars] = ';';
            ExistingString->LengthInChars++;
        }
        memcpy(&ExistingString->StartOfString[ExistingString->LengthInChars], NewComponent->StartOfString, NewComponent->LengthInChars * sizeof(TCHAR));
        ExistingString->LengthInChars += NewComponent->LengthInChars;
        ExistingString->StartOfString[ExistingString->LengthInChars] = '\0';
    }
    return TRUE;
}

/**
 Add a component to a semicolon delimited environment variable if it's not
 already there and return the result as a string.

 @param EnvironmentVariable The environment variable to update.

 @param NewComponent The component to add.

 @param InsertAtFront If TRUE, insert the new component at the front of the
        list.  If FALSE, add the component to the end of the list.

 @param Result On successful completion, updated to contain the merged string.

 @return TRUE to indicate success, FALSE on failure.
 */
__success(return)
BOOL
YoriLibAddEnvironmentComponentReturnString(
    __in PYORI_STRING EnvironmentVariable,
    __in PYORI_STRING NewComponent,
    __in BOOL InsertAtFront,
    __out PYORI_STRING Result
    )
{
    DWORD EnvVarLength;

    ASSERT(YoriLibIsStringNullTerminated(EnvironmentVariable));

    //
    //  The contents of the environment variable.  Allocate enough
    //  space to append the specified directory in case we need to.
    //

    EnvVarLength = GetEnvironmentVariable(EnvironmentVariable->StartOfString, NULL, 0);
    if (!YoriLibAllocateString(Result, EnvVarLength + 1 + NewComponent->LengthInChars + 1)) {
        return FALSE;
    }

    Result->StartOfString[0] = '\0';
    Result->LengthInChars = GetEnvironmentVariable(EnvironmentVariable->StartOfString, Result->StartOfString, Result->LengthAllocated);

    YoriLibAddEnvironmentComponentToString(Result, NewComponent, InsertAtFront);
    return TRUE;
}


/**
 Add a component to a semicolon delimited environment variable if it's not
 already there.

 @param EnvironmentVariable The environment variable to update.

 @param NewComponent The component to add.

 @param InsertAtFront If TRUE, insert the new component at the front of the
        list.  If FALSE, add the component to the end of the list.

 @return TRUE to indicate success, FALSE on failure.
 */
__success(return)
BOOL
YoriLibAddEnvironmentComponent(
    __in LPTSTR EnvironmentVariable,
    __in PYORI_STRING NewComponent,
    __in BOOL InsertAtFront
    )
{
    YORI_STRING EnvData;
    YORI_STRING YsEnvironmentVariable;

    YoriLibConstantString(&YsEnvironmentVariable, EnvironmentVariable);

    if (!YoriLibAddEnvironmentComponentReturnString(&YsEnvironmentVariable, NewComponent, InsertAtFront, &EnvData)) {
        return FALSE;
    }

    if (!SetEnvironmentVariable(EnvironmentVariable, EnvData.StartOfString)) {
        YoriLibFreeStringContents(&EnvData);
        return FALSE;
    }

    YoriLibFreeStringContents(&EnvData);
    return TRUE;
}

/**
 Remove a component from a semicolon delimited string buffer if it's already
 there and return the combined string.

 @param String The string buffer that contains semicolon delimited elements.

 @param ComponentToRemove The component to remove.

 @param Result On successful completion, updated to contain the adjusted
        string.

 @return TRUE to indicate success, FALSE on failure.
 */
__success(return)
BOOL
YoriLibRemoveEnvironmentComponentFromString(
    __in PYORI_STRING String,
    __in PYORI_STRING ComponentToRemove,
    __out PYORI_STRING Result
    )
{
    LPTSTR NewPathData;
    LPTSTR ThisPath;
    TCHAR * TokCtx;
    DWORD NewOffset;
    DWORD CharsPopulated;

    ASSERT(YoriLibIsStringNullTerminated(String));

    NewPathData = YoriLibReferencedMalloc((String->LengthInChars + 1) * sizeof(TCHAR));
    if (NewPathData == NULL) {
        return FALSE;
    }

    NewOffset = 0;

    ThisPath = _tcstok_s(String->StartOfString, _T(";"), &TokCtx);
    while (ThisPath != NULL) {
        if (ThisPath[0] != '\0') {
            if (YoriLibCompareStringWithLiteralInsensitive(ComponentToRemove, ThisPath) != 0) {
                if (NewOffset != 0) {
                    CharsPopulated = YoriLibSPrintf(&NewPathData[NewOffset], _T(";%s"), ThisPath);
                } else {
                    CharsPopulated = YoriLibSPrintf(&NewPathData[NewOffset], _T("%s"), ThisPath);
                }

                if (CharsPopulated == (DWORD)-1) {
                    YoriLibDereference(NewPathData);
                    return FALSE;
                }
                NewOffset += CharsPopulated;
            }
        }
        if (TokCtx != NULL) {
            TokCtx--;
            ASSERT(*TokCtx == '\0');
            *TokCtx = ';';
            TokCtx++;
        }
        ThisPath = _tcstok_s(NULL, _T(";"), &TokCtx);
    }

    //
    //  Normally this would be NULL terminated by Printf, but if that wasn't
    //  invoked at least once nothing terminated the destination buffer, so
    //  do it explicitly here.
    //

    if (NewOffset == 0) {
        NewPathData[NewOffset] = '\0';
    }

    YoriLibInitEmptyString(Result);
    Result->MemoryToFree = NewPathData;
    Result->StartOfString = NewPathData;
    Result->LengthInChars = NewOffset;
    if (NewOffset > 0) {
        Result->LengthAllocated = NewOffset + 1;
    } else {
        Result->LengthAllocated = 0;
    }

    return TRUE;
}

/**
 Remove a component from a semicolon delimited environment variable if it's
 already there and return the combined string.

 @param EnvironmentVariable The environment variable to update.

 @param ComponentToRemove The component to remove.

 @param Result On successful completion, updated to contain the adjusted
        string.

 @return TRUE to indicate success, FALSE on failure.
 */
__success(return)
BOOL
YoriLibRemoveEnvironmentComponentReturnString(
    __in PYORI_STRING EnvironmentVariable,
    __in PYORI_STRING ComponentToRemove,
    __out PYORI_STRING Result
    )
{
    YORI_STRING PathData;
    DWORD PathLength;
    BOOL Success;

    ASSERT(YoriLibIsStringNullTerminated(EnvironmentVariable));

    //
    //  The contents of the environment variable.  Allocate enough
    //  space to append the specified directory in case we need to.
    //

    PathLength = GetEnvironmentVariable(EnvironmentVariable->StartOfString, NULL, 0);
    if (!YoriLibAllocateString(&PathData, PathLength + 1)) {
        return FALSE;
    }

    PathData.StartOfString[0] = '\0';
    PathData.LengthInChars = GetEnvironmentVariable(EnvironmentVariable->StartOfString, PathData.StartOfString, PathData.LengthAllocated);

    Success = YoriLibRemoveEnvironmentComponentFromString(&PathData, ComponentToRemove, Result);

    YoriLibFreeStringContents(&PathData);

    return Success;
}

/**
 Remove a component from a semicolon delimited environment variable if it's
 already there.

 @param EnvironmentVariable The environment variable to update.

 @param ComponentToRemove The component to remove.

 @return TRUE to indicate success, FALSE on failure.
 */
__success(return)
BOOL
YoriLibRemoveEnvironmentComponent(
    __in LPTSTR EnvironmentVariable,
    __in PYORI_STRING ComponentToRemove
    )
{
    YORI_STRING CombinedString;
    LPTSTR ValueToSet;
    YORI_STRING YsEnvironmentVariable;

    YoriLibConstantString(&YsEnvironmentVariable, EnvironmentVariable);

    if (!YoriLibRemoveEnvironmentComponentReturnString(&YsEnvironmentVariable, ComponentToRemove, &CombinedString)) {
        return FALSE;
    }

    //
    //  If no data was copied forward, delete the variable.  Note NewPathData
    //  contains uninitialized data at this point when that occurs.
    //

    if (CombinedString.LengthAllocated > 0) {
        ValueToSet = CombinedString.StartOfString;
    } else {
        ValueToSet = NULL;
    }

    if (!SetEnvironmentVariable(EnvironmentVariable, ValueToSet)) {
        YoriLibFreeStringContents(&CombinedString);
        return FALSE;
    }

    YoriLibFreeStringContents(&CombinedString);
    return TRUE;
}


// vim:sw=4:ts=4:et:


<|start_filename|>lines/lines.c<|end_filename|>
/**
 * @file lines/lines.c
 *
 * Yori shell display count of lines in files
 *
 * Copyright (c) 2017-2019 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 Help text to display to the user.
 */
const
CHAR strLinesHelpText[] =
        "\n"
        "Count the number of lines in one or more files.\n"
        "\n"
        "LINES [-license] [-b] [-s] [-t] [<file>...]\n"
        "\n"
        "   -b             Use basic search criteria for files only\n"
        "   -s             Process files from all subdirectories\n"
        "   -t             Display total line count of all files\n";

/**
 Display usage text to the user.
 */
BOOL
LinesHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Lines %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strLinesHelpText);
    return TRUE;
}

/**
 Context passed to the callback which is invoked for each file found.
 */
typedef struct _LINES_CONTEXT {

    /**
     TRUE to indicate that the only output should be the total number of
     lines, after all files have been processed.
     */
    BOOLEAN SummaryOnly;

    /**
     TRUE to indicate that files are being enumerated recursively.
     */
    BOOLEAN Recursive;

    /**
     The first error encountered when enumerating objects from a single arg.
     This is used to preserve file not found/path not found errors so that
     when the program falls back to interpreting the argument as a literal,
     if that still doesn't work, this is the error code that is displayed.
     */
    DWORD SavedErrorThisArg;

    /**
     Records the total number of files processed.
     */
    LONGLONG FilesFound;

    /**
     Records the total number of files processed within a single command line
     argument.
     */
    LONGLONG FilesFoundThisArg;

    /**
     Records the number of lines found in a single file.
     */
    LONGLONG FileLinesFound;

    /**
     Records the total number of lines processed for all files.
     */
    LONGLONG TotalLinesFound;
} LINES_CONTEXT, *PLINES_CONTEXT;

/**
 Count the lines in an opened stream.

 @param hSource Handle to the source.

 @param LinesContext Specifies the context to record line count information.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
LinesProcessStream(
    __in HANDLE hSource,
    __in PLINES_CONTEXT LinesContext
    )
{
    PVOID LineContext = NULL;
    YORI_STRING LineString;

    YoriLibInitEmptyString(&LineString);

    LinesContext->FilesFound++;
    LinesContext->FilesFoundThisArg++;
    LinesContext->FileLinesFound = 0;

    while (TRUE) {

        if (!YoriLibReadLineToString(&LineString, &LineContext, hSource)) {
            break;
        }

        LinesContext->FileLinesFound++;
    }

    YoriLibLineReadCloseOrCache(LineContext);
    YoriLibFreeStringContents(&LineString);

    LinesContext->TotalLinesFound += LinesContext->FileLinesFound;
    return TRUE;
}

/**
 A callback that is invoked when a file is found that matches a search criteria
 specified in the set of strings to enumerate.

 @param FilePath Pointer to the file path that was found.

 @param FileInfo Information about the file.  This can be NULL if the file
        was not found by enumeration.

 @param Depth Specifies the recursion depth.  Ignored in this application.

 @param Context Pointer to the lines context structure indicating the action
        to perform and populated with the file and line count found.

 @return TRUE to continute enumerating, FALSE to abort.
 */
BOOL
LinesFileFoundCallback(
    __in PYORI_STRING FilePath,
    __in_opt PWIN32_FIND_DATA FileInfo,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    HANDLE FileHandle;
    PLINES_CONTEXT LinesContext = (PLINES_CONTEXT)Context;

    UNREFERENCED_PARAMETER(Depth);

    ASSERT(YoriLibIsStringNullTerminated(FilePath));

    if (FileInfo == NULL ||
        (FileInfo->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) {

        FileHandle = CreateFile(FilePath->StartOfString,
                                GENERIC_READ,
                                FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                                NULL,
                                OPEN_EXISTING,
                                FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS,
                                NULL);

        if (FileHandle == NULL || FileHandle == INVALID_HANDLE_VALUE) {
            if (LinesContext->SavedErrorThisArg == ERROR_SUCCESS) {
                DWORD LastError = GetLastError();
                LPTSTR ErrText = YoriLibGetWinErrorText(LastError);
                YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("lines: open of %y failed: %s"), FilePath, ErrText);
                YoriLibFreeWinErrorText(ErrText);
            }
            return TRUE;
        }

        LinesContext->SavedErrorThisArg = ERROR_SUCCESS;
        LinesProcessStream(FileHandle, LinesContext);

        if (!LinesContext->SummaryOnly) {
            YORI_STRING StringFormOfLineCount;
            YORI_STRING UnescapedFilePath;
            TCHAR StackBuffer[16];

            YoriLibInitEmptyString(&StringFormOfLineCount);
            StringFormOfLineCount.StartOfString = StackBuffer;
            StringFormOfLineCount.LengthAllocated = sizeof(StackBuffer)/sizeof(StackBuffer[0]);
            YoriLibNumberToString(&StringFormOfLineCount, LinesContext->FileLinesFound, 10, 3, ',');
            YoriLibInitEmptyString(&UnescapedFilePath);
            YoriLibUnescapePath(FilePath, &UnescapedFilePath);
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%16y %y\n"), &StringFormOfLineCount, &UnescapedFilePath);
            YoriLibFreeStringContents(&StringFormOfLineCount);
            YoriLibFreeStringContents(&UnescapedFilePath);
        }

        CloseHandle(FileHandle);
    }

    return TRUE;
}

/**
 A callback that is invoked when a directory cannot be successfully enumerated.

 @param FilePath Pointer to the file path that could not be enumerated.

 @param ErrorCode The Win32 error code describing the failure.

 @param Depth Recursion depth, ignored in this application.

 @param Context Pointer to the context block indicating whether the
        enumeration was recursive.  Recursive enumerates do not complain
        if a matching file is not in every single directory, because
        common usage expects files to be in a subset of directories only.

 @return TRUE to continute enumerating, FALSE to abort.
 */
BOOL
LinesFileEnumerateErrorCallback(
    __in PYORI_STRING FilePath,
    __in DWORD ErrorCode,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    YORI_STRING UnescapedFilePath;
    BOOL Result = FALSE;
    PLINES_CONTEXT LinesContext = (PLINES_CONTEXT)Context;

    UNREFERENCED_PARAMETER(Depth);

    YoriLibInitEmptyString(&UnescapedFilePath);
    if (!YoriLibUnescapePath(FilePath, &UnescapedFilePath)) {
        UnescapedFilePath.StartOfString = FilePath->StartOfString;
        UnescapedFilePath.LengthInChars = FilePath->LengthInChars;
    }

    if (ErrorCode == ERROR_FILE_NOT_FOUND || ErrorCode == ERROR_PATH_NOT_FOUND) {
        if (!LinesContext->Recursive) {
            LinesContext->SavedErrorThisArg = ErrorCode;
        }
        Result = TRUE;
    } else {
        LPTSTR ErrText = YoriLibGetWinErrorText(ErrorCode);
        YORI_STRING DirName;
        LPTSTR FilePart;
        YoriLibInitEmptyString(&DirName);
        DirName.StartOfString = UnescapedFilePath.StartOfString;
        FilePart = YoriLibFindRightMostCharacter(&UnescapedFilePath, '\\');
        if (FilePart != NULL) {
            DirName.LengthInChars = (DWORD)(FilePart - DirName.StartOfString);
        } else {
            DirName.LengthInChars = UnescapedFilePath.LengthInChars;
        }
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Enumerate of %y failed: %s"), &DirName, ErrText);
        YoriLibFreeWinErrorText(ErrText);
    }
    YoriLibFreeStringContents(&UnescapedFilePath);
    return Result;
}


#ifdef YORI_BUILTIN
/**
 The main entrypoint for the lines builtin command.
 */
#define ENTRYPOINT YoriCmd_LINES
#else
/**
 The main entrypoint for the lines standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the lines cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the child process on success, or failure if the child
         could not be launched.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    DWORD MatchFlags;
    BOOL BasicEnumeration = FALSE;
    LINES_CONTEXT LinesContext;
    YORI_STRING Arg;

    ZeroMemory(&LinesContext, sizeof(LinesContext));

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                LinesHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2017-2019"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("b")) == 0) {
                BasicEnumeration = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("s")) == 0) {
                LinesContext.Recursive = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("t")) == 0) {
                LinesContext.SummaryOnly = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                StartArg = i + 1;
                ArgumentUnderstood = TRUE;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

#if YORI_BUILTIN
    YoriLibCancelEnable();
#endif

    //
    //  Attempt to enable backup privilege so an administrator can access more
    //  objects successfully.
    //

    YoriLibEnableBackupPrivilege();

    //
    //  If no file name is specified, use stdin; otherwise open
    //  the file and use that
    //

    if (StartArg == 0 || StartArg == ArgC) {
        if (YoriLibIsStdInConsole()) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("No file or pipe for input\n"));
            return EXIT_FAILURE;
        }

        LinesContext.SummaryOnly = TRUE;

        LinesProcessStream(GetStdHandle(STD_INPUT_HANDLE), &LinesContext);
    } else {
        MatchFlags = YORILIB_FILEENUM_RETURN_FILES | YORILIB_FILEENUM_DIRECTORY_CONTENTS;
        if (LinesContext.Recursive) {
            MatchFlags |= YORILIB_FILEENUM_RECURSE_BEFORE_RETURN | YORILIB_FILEENUM_RECURSE_PRESERVE_WILD;
        }
        if (BasicEnumeration) {
            MatchFlags |= YORILIB_FILEENUM_BASIC_EXPANSION;
        }

        for (i = StartArg; i < ArgC; i++) {

            LinesContext.FilesFoundThisArg = 0;
            LinesContext.SavedErrorThisArg = ERROR_SUCCESS;

            YoriLibForEachStream(&ArgV[i],
                                 MatchFlags,
                                 0,
                                 LinesFileFoundCallback,
                                 LinesFileEnumerateErrorCallback,
                                 &LinesContext);

            if (LinesContext.FilesFoundThisArg == 0) {
                YORI_STRING FullPath;
                YoriLibInitEmptyString(&FullPath);
                if (YoriLibUserStringToSingleFilePath(&ArgV[i], TRUE, &FullPath)) {
                    LinesFileFoundCallback(&FullPath, NULL, 0, &LinesContext);
                    YoriLibFreeStringContents(&FullPath);
                }
                if (LinesContext.SavedErrorThisArg != ERROR_SUCCESS) {
                    YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("File or directory not found: %y\n"), &ArgV[i]);
                }
            }
        }
    }

#if !YORI_BUILTIN
    YoriLibLineReadCleanupCache();
#endif

    if (LinesContext.FilesFound == 0) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("lines: no matching files found\n"));
        return EXIT_FAILURE;
    } else if (LinesContext.FilesFound > 1 || LinesContext.SummaryOnly) {
        YORI_STRING StringFormOfLineCount;

        YoriLibInitEmptyString(&StringFormOfLineCount);
        YoriLibNumberToString(&StringFormOfLineCount, LinesContext.TotalLinesFound, 10, 3, ',');
        YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%y\n"), &StringFormOfLineCount);
        YoriLibFreeStringContents(&StringFormOfLineCount);
    }

    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>sh/Makefile<|end_filename|>

BINARIES=yori.exe yorimin.exe oneyori.exe
INITFILES=YoriInit.d\*

!INCLUDE "..\config\common.mk"
YORI_BIN_VER_MAJOR=$(YORI_VER_MAJOR)
YORI_BIN_VER_MINOR=$(YORI_VER_MINOR)

YORIMINPDB=
YORISTDPDB=

!IF $(PDB)==1
MIN_PDB=/Pdb:yorimin.pdb
STD_PDB=/Pdb:yori.pdb
FULL_PDB=/Pdb:oneyori.pdb
!ENDIF

OBJS=\
	alias.obj        \
	api.obj          \
	builtin.obj      \
	complete.obj     \
	env.obj          \
	exec.obj         \
	history.obj      \
	input.obj        \
	job.obj          \
	main.obj         \
	parse.obj        \
	prompt.obj       \
	restart.obj      \
	window.obj       \
	yori.obj         \

BUILTINTABLE_OBJS=\
	yoristd.obj      \
	yorinone.obj     \
	yorifull.obj     \

STD_BUILTINLIBS=\
	..\builtins\builtins.lib \
	$(YORIWIN)    \
	..\echo\builtins.lib     \
	..\for\builtins.lib      \
	..\intcmp\builtins.lib   \
	..\nice\builtins.lib     \
	..\strcmp\builtins.lib   \

FULL_BUILTINLIBS=\
	$(STD_BUILTINLIBS)       \
	yorifull.lib             \

compile: $(OBJS) $(BUILTINTABLE_OBJS)

yorimin.exe: $(OBJS) yorinone.obj yorimin.def $(YORILIB) $(YORICRT) $(YORISH)
	@$(LINK) $(LDFLAGS) -entry:$(YENTRY) $(OBJS) yorinone.obj $(LIBS) $(CRTLIB) $(YORILIB) $(YORISH) -version:$(YORI_VER_MAJOR).$(YORI_VER_MINOR) -def:$(@B).def $(MIN_PDB) -out:$@

yori.exe: $(OBJS) yoristd.obj yori.def $(YORILIB) $(YORICRT) $(YORISH) $(STD_BUILTINLIBS)
	@$(LINK) $(LDFLAGS) -entry:$(YENTRY) $(OBJS) $(LIBS) yoristd.obj $(CRTLIB) $(YORILIB) $(YORISH) $(STD_BUILTINLIBS) -version:$(YORI_VER_MAJOR).$(YORI_VER_MINOR) -def:$(@B).def $(STD_PDB) -out:$@

!IFDEF _YMAKE_VER
ONEYORI_DEPLIST=@yorifull.lst
!ENDIF

oneyori.exe: $(OBJS) yorifull.obj oneyori.def yorifull.lst $(YORILIB) $(YORICRT) $(YORISH) $(STD_BUILTINLIBS) $(ONEYORI_DEPLIST)
	@$(LIB32) $(LIBFLAGS) @yorifull.lst /out:yorifull.lib
	@$(LINK) $(LDFLAGS) -entry:$(YENTRY) $(OBJS) $(LIBS) yorifull.obj $(CRTLIB) $(YORILIB) $(YORISH) $(FULL_BUILTINLIBS) $(FDILIB) -version:$(YORI_VER_MAJOR).$(YORI_VER_MINOR) -def:$(@B).def $(FULL_PDB) -out:$@



<|start_filename|>touch/touch.c<|end_filename|>
/**
 * @file touch/touch.c
 *
 * Yori create files or update timestamps
 *
 * Copyright (c) 2018 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 Help text to display to the user.
 */
const
CHAR strTouchHelpText[] =
        "\n"
        "Create files or update timestamps.\n"
        "\n"
        "TOUCH [-license] [-a] [-b] [-c] [-e] [-f size] [-s] [-t <date and time>]\n"
        "      [-w] <file>...\n"
        "\n"
        "   -a             Update last access time\n"
        "   -b             Use basic search criteria for files only\n"
        "   -c             Update create time\n"
        "   -e             Only update existing files\n"
        "   -f             Create new file with specified file size\n"
        "   -s             Process files from all subdirectories\n"
        "   -t             Specify the timestamp to set\n"
        "   -w             Update write time\n";

/**
 Display usage text to the user.
 */
BOOL
TouchHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Touch %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strTouchHelpText);
    return TRUE;
}

/**
 Context passed to the callback which is invoked for each file found.
 */
typedef struct _TOUCH_CONTEXT {

    /**
     Specifies the new creation time to apply to each file.
     */
    FILETIME NewCreationTime;

    /**
     Specifies the new access time to apply to each file.
     */
    FILETIME NewAccessTime;

    /**
     Specifies the new write time to apply to each file.
     */
    FILETIME NewWriteTime;

    /**
     File size for newly created files.
     */
    LARGE_INTEGER NewFileSize;

    /**
     Counts the number of files processed in an enumerate.  If this is zero,
     the program assumes the request is to create a new file.
     */
    ULONG FilesFoundThisArg;

    /**
     If TRUE, only existing files should be modified, and no new files should
     be created.
     */
    BOOL ExistingOnly;

} TOUCH_CONTEXT, *PTOUCH_CONTEXT;

/**
 A callback that is invoked when a file is found that matches a search criteria
 specified in the set of strings to enumerate.

 @param FilePath Pointer to the file path that was found.

 @param FileInfo Information about the file.  Note in this application this can
        be NULL when it is operating on files that do not yet exist.

 @param Depth Specifies the recursion depth.  Ignored in this application.

 @param Context Pointer to the touch context structure indicating the
        action to perform and populated with the file and line count found.

 @return TRUE to continute enumerating, FALSE to abort.
 */
BOOL
TouchFileFoundCallback(
    __in PYORI_STRING FilePath,
    __in_opt PWIN32_FIND_DATA FileInfo,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    HANDLE FileHandle;
    DWORD DesiredAccess;
    PTOUCH_CONTEXT TouchContext = (PTOUCH_CONTEXT)Context;

    UNREFERENCED_PARAMETER(Depth);
    UNREFERENCED_PARAMETER(FileInfo);

    ASSERT(YoriLibIsStringNullTerminated(FilePath));

    DesiredAccess = GENERIC_READ | FILE_WRITE_ATTRIBUTES;
    if (FileInfo == NULL) {
        DesiredAccess |= GENERIC_WRITE;
    }

    FileHandle = CreateFile(FilePath->StartOfString,
                            DesiredAccess,
                            FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                            NULL,
                            TouchContext->ExistingOnly?OPEN_EXISTING:OPEN_ALWAYS,
                            FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS,
                            NULL);

    if (FileHandle == NULL || FileHandle == INVALID_HANDLE_VALUE) {
        DWORD LastError = GetLastError();
        LPTSTR ErrText = YoriLibGetWinErrorText(LastError);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("touch: open of %y failed: %s"), FilePath, ErrText);
        YoriLibFreeWinErrorText(ErrText);
        return TRUE;
    }

    TouchContext->FilesFoundThisArg++;

    if (FileInfo == NULL) {
        if (TouchContext->NewFileSize.QuadPart != 0) {
            SetFilePointer(FileHandle, TouchContext->NewFileSize.LowPart, &TouchContext->NewFileSize.HighPart, FILE_BEGIN);
            if (!SetEndOfFile(FileHandle)) {
                DWORD LastError = GetLastError();
                LPTSTR ErrText = YoriLibGetWinErrorText(LastError);
                YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("touch: setting file size of %y failed: %s"), FilePath, ErrText);
                YoriLibFreeWinErrorText(ErrText);

                //
                //  Intentional fallout
                //
            }
        }
    }

    if (!SetFileTime(FileHandle, &TouchContext->NewCreationTime, &TouchContext->NewAccessTime, &TouchContext->NewWriteTime)) {
        DWORD LastError = GetLastError();
        LPTSTR ErrText = YoriLibGetWinErrorText(LastError);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("touch: updating timestamps of %y failed: %s"), FilePath, ErrText);
        YoriLibFreeWinErrorText(ErrText);

        //
        //  Intentional fallout
        //
    }

    CloseHandle(FileHandle);
    return TRUE;
}

#ifdef YORI_BUILTIN
/**
 The main entrypoint for the touch builtin command.
 */
#define ENTRYPOINT YoriCmd_TOUCH
#else
/**
 The main entrypoint for the touch standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the touch cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the child process on success, or failure if the child
         could not be launched.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    DWORD MatchFlags;
    BOOL Recursive = FALSE;
    BOOL BasicEnumeration = FALSE;
    BOOL UpdateLastAccess = FALSE;
    BOOL UpdateCreationTime = FALSE;
    BOOL UpdateWriteTime = FALSE;
    SYSTEMTIME CurrentSystemTime;
    FILETIME TimestampToUse;
    TOUCH_CONTEXT TouchContext;
    YORI_STRING Arg;

    ZeroMemory(&TouchContext, sizeof(TouchContext));
    GetSystemTime(&CurrentSystemTime);
    if (!SystemTimeToFileTime(&CurrentSystemTime, &TimestampToUse)) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("touch: could not query system time\n"));
        return EXIT_FAILURE;
    }

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                TouchHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2018"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("a")) == 0) {
                UpdateLastAccess = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("b")) == 0) {
                BasicEnumeration = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("c")) == 0) {
                UpdateCreationTime = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("e")) == 0) {
                TouchContext.ExistingOnly = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("f")) == 0) {
                if (i + 1 < ArgC) {
                    TouchContext.NewFileSize = YoriLibStringToFileSize(&ArgV[i + 1]);
                    ArgumentUnderstood = TRUE;
                    i++;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("s")) == 0) {
                Recursive = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("t")) == 0) {
                if (i + 1 < ArgC) {
                    SYSTEMTIME NewTime;
                    FILETIME LocalNewTime;
                    ZeroMemory(&NewTime, sizeof(NewTime));
                    if (YoriLibStringToDateTime(&ArgV[i + 1], &NewTime)) {
                        YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Setting time to %i/%i/%i:%i:%i:%i\n"), NewTime.wYear, NewTime.wMonth, NewTime.wDay, NewTime.wHour, NewTime.wMinute, NewTime.wSecond);
                        if (!SystemTimeToFileTime(&NewTime, &LocalNewTime) || !LocalFileTimeToFileTime(&LocalNewTime, &TimestampToUse)) {
                            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Could not parse time: %y\n"), &ArgV[i + 1]);
                        }
                        ArgumentUnderstood = TRUE;
                    }
                    i++;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("w")) == 0) {
                UpdateWriteTime = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                StartArg = i + 1;
                ArgumentUnderstood = TRUE;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    if (!UpdateLastAccess && !UpdateCreationTime && !UpdateWriteTime) {
        UpdateWriteTime = TRUE;
    }

    if (UpdateLastAccess) {
        TouchContext.NewAccessTime = TimestampToUse;
    } else {
        TouchContext.NewAccessTime.dwLowDateTime = (DWORD)-1;
        TouchContext.NewAccessTime.dwHighDateTime = (DWORD)-1;
    }

    if (UpdateCreationTime) {
        TouchContext.NewCreationTime = TimestampToUse;
    } else {
        TouchContext.NewCreationTime.dwLowDateTime = (DWORD)-1;
        TouchContext.NewCreationTime.dwHighDateTime = (DWORD)-1;
    }

    if (UpdateWriteTime) {
        TouchContext.NewWriteTime = TimestampToUse;
    } else {
        TouchContext.NewWriteTime.dwLowDateTime = (DWORD)-1;
        TouchContext.NewWriteTime.dwHighDateTime = (DWORD)-1;
    }

    //
    //  If no file name is specified, use stdin; otherwise open
    //  the file and use that
    //

    if (StartArg == 0 || StartArg == ArgC) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("touch: missing argument\n"));
        return EXIT_FAILURE;
    } else {
        MatchFlags = YORILIB_FILEENUM_RETURN_FILES | YORILIB_FILEENUM_RETURN_DIRECTORIES;
        if (Recursive) {
            MatchFlags |= YORILIB_FILEENUM_RECURSE_BEFORE_RETURN | YORILIB_FILEENUM_RECURSE_PRESERVE_WILD;
        }
        if (BasicEnumeration) {
            MatchFlags |= YORILIB_FILEENUM_BASIC_EXPANSION;
        }

        for (i = StartArg; i < ArgC; i++) {

            TouchContext.FilesFoundThisArg = 0;
            YoriLibForEachFile(&ArgV[i], MatchFlags, 0, TouchFileFoundCallback, NULL, &TouchContext);
            if (TouchContext.FilesFoundThisArg == 0) {
                YORI_STRING FullPath;
                YoriLibInitEmptyString(&FullPath);
                if (YoriLibUserStringToSingleFilePath(&ArgV[i], TRUE, &FullPath)) {
                    TouchFileFoundCallback(&FullPath, NULL, 0, &TouchContext);
                    YoriLibFreeStringContents(&FullPath);
                }
            }
        }
    }

    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>cvtvt/main.c<|end_filename|>
/**
 * @file cvtvt/main.c
 *
 * Main entry point code to convert and process VT100/ANSI escape sequences.
 *
 * Copyright (c) 2015-2018 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "cvtvt.h"

/**
 Help text to display for this application.
 */
const
CHAR strCvtvtHelpText[] =
        "\n"
        "Converts text with VT100 color escapes into another format.\n"
        "\n"
        "CVTVT [-license] [Options] [-exec binary|filename]\n"
        "\n"
        " Options include:\n"
        "   -exec binary   Run process and pipe its output into cvtvt\n"
        "   -html4         Generate output with FONT tags (no backgrounds)\n"
        "   -html5         Generate output with CSS\n"
        "   -rtf           Generate output as RTF\n"
        "   -text          Generate output as plain text\n"
        "   -win32         Convert to native Win32\n"
        "\n"
        " If filename and exec are not specified, operates on standard"
        " input.\n";

/**
 Display help text for this application.
 */
BOOL
CvtvtUsage(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Cvtvt %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strCvtvtHelpText);
    return TRUE;
}

/**
 Pump any incoming data from standard input to a specified pipe which will
 be used as the input for a child process.

 @param Context The context for the child, which in this case is the handle
        for the pipe to write data to.

 @return Exit code for the thread, currently unused.
 */
DWORD WINAPI
CvtvtInputPumpThread(
    __in LPVOID Context
    )
{
    HANDLE hOutput = (HANDLE)Context;
    HANDLE hInput;
    UCHAR StupidBuffer[256];
    DWORD BytesRead;
    DWORD BytesWritten;

    hInput = GetStdHandle(STD_INPUT_HANDLE);

    SetConsoleMode(hInput, ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT);

    while (ReadFile(hInput, StupidBuffer, sizeof(StupidBuffer), &BytesRead, NULL)) {
        if (!WriteFile(hOutput, StupidBuffer, BytesRead, &BytesWritten, NULL)) {
            break;
        }
    }

    return 0;
}

/**
 Launch a specified child process, with input and output handles created as
 pipes.  This function returns the ends of the pipe used to communicate with
 the child process.

 @param PathName Pointer to the string for the program to launch.  This will
        be located in the path before executing.

 @param CmdLine Pointer to the entire command line including arguments.

 @param hSource On successful completion, updated to contain a handle that
        reads the output from the child process.

 @param hControl On successful completion, updated to contain a handle that
        writes the input into the child process.

 @return Win32 error code, ERROR_SUCCESS for success.
 */
__success(return == ERROR_SUCCESS)
DWORD
CvtvtLaunchChildProcess(
    __in PYORI_STRING PathName,
    __in PYORI_STRING CmdLine,
    __out PHANDLE hSource,
    __out PHANDLE hControl
    )
{
    HANDLE hProcessInput;
    HANDLE hProcessOutput;
    HANDLE hParentInput;
    HANDLE hParentOutput;
    STARTUPINFO StartupInfo;
    PROCESS_INFORMATION ProcessInfo;
    TCHAR szTermVar[256];
    DWORD dwConsoleMode;
    DWORD Err;
    CONSOLE_SCREEN_BUFFER_INFO ConsoleInfo;
    YORI_STRING ExpandedCmd;
    YORI_STRING FoundExecutable;

    if (!YoriLibExpandHomeDirectories(PathName, &ExpandedCmd)) {
        YoriLibCloneString(&ExpandedCmd, PathName);
    }

    YoriLibInitEmptyString(&FoundExecutable);
    if (!YoriLibLocateExecutableInPath(&ExpandedCmd, NULL, NULL, &FoundExecutable)) {
        YoriLibFreeStringContents(&ExpandedCmd);
        return ERROR_FILE_NOT_FOUND;
    }

    YoriLibFreeStringContents(&ExpandedCmd);

    if (FoundExecutable.LengthInChars == 0) {
        YoriLibFreeStringContents(&FoundExecutable);
        return ERROR_FILE_NOT_FOUND;
    }

    ZeroMemory(&StartupInfo, sizeof(StartupInfo));
    ZeroMemory(&ProcessInfo, sizeof(ProcessInfo));

    if (!CreatePipe(&hProcessInput, &hParentOutput, NULL, 2048)) {
        Err = GetLastError();
        YoriLibFreeStringContents(&FoundExecutable);
        return Err;
    }

    if (!CreatePipe(&hParentInput, &hProcessOutput, NULL, 2048)) {
        Err = GetLastError();
        CloseHandle(hProcessInput);
        CloseHandle(hParentOutput);
        YoriLibFreeStringContents(&FoundExecutable);
        return Err;
    }

    if (GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &ConsoleInfo)) {
        YoriLibSPrintf(szTermVar, _T("%i"), ConsoleInfo.srWindow.Right - ConsoleInfo.srWindow.Left + 1);
        SetEnvironmentVariable(_T("COLUMNS"), szTermVar);
        YoriLibSPrintf(szTermVar, _T("%i"), ConsoleInfo.srWindow.Bottom - ConsoleInfo.srWindow.Top + 1);
        SetEnvironmentVariable(_T("LINES"), szTermVar);

        if (GetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), &dwConsoleMode)) {

            //
            //  If the window right edge is the end of the buffer, then
            //  auto line wrap is in effect if the console has it
            //  enabled.  If neither of these is true, then apps must
            //  explicitly emit newlines.
            //

            if (ConsoleInfo.dwSize.X != (ConsoleInfo.srWindow.Right - ConsoleInfo.srWindow.Left + 1)) {
                dwConsoleMode = 0;
            }
            YoriLibSPrintf(szTermVar, _T("color;extendedchars%s"), dwConsoleMode&ENABLE_WRAP_AT_EOL_OUTPUT?_T(";autolinewrap"):_T(""));
            SetEnvironmentVariable(_T("YORITERM"), szTermVar);
        }

    }

    YoriLibMakeInheritableHandle(hProcessInput, &hProcessInput);
    YoriLibMakeInheritableHandle(hProcessOutput, &hProcessOutput);

    StartupInfo.dwFlags = STARTF_USESTDHANDLES;
    StartupInfo.hStdInput = hProcessInput;
    StartupInfo.hStdOutput = hProcessOutput;
    StartupInfo.hStdError = hProcessOutput;

    SetEnvironmentVariable(_T("PROMPT"), _T("$e[31;1m$p$e[37m$g"));

    if (!CreateProcess(FoundExecutable.StartOfString,
                       CmdLine->StartOfString,
                       NULL,
                       NULL,
                       TRUE,
                       CREATE_DEFAULT_ERROR_MODE,
                       NULL,
                       NULL,
                       &StartupInfo,
                       &ProcessInfo)) {

        YoriLibFreeStringContents(&FoundExecutable);

        CloseHandle(hProcessInput);
        CloseHandle(hProcessOutput);

        CloseHandle(hParentInput);
        CloseHandle(hParentOutput);

        return GetLastError();
    }

    YoriLibFreeStringContents(&FoundExecutable);

    CloseHandle(hProcessInput);
    CloseHandle(hProcessOutput);

    CloseHandle(ProcessInfo.hProcess);
    CloseHandle(ProcessInfo.hThread);

    *hControl = hParentOutput;
    *hSource = hParentInput;
    return ERROR_SUCCESS;
}

#ifdef YORI_BUILTIN
/**
 The main entrypoint for the cvtvt builtin command.
 */
#define ENTRYPOINT YoriCmd_CVTVT
#else
/**
 The main entrypoint for the cvtvt standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The master entrypoint for cvtvt.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments to cvtvt.

 @return Exit code for the application, typically zero for success and
         nonzero for failure.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    HANDLE hSource = INVALID_HANDLE_VALUE;
    HANDLE hOutput = INVALID_HANDLE_VALUE;
    HANDLE hControl = INVALID_HANDLE_VALUE;
    HANDLE InputPumpThread = NULL;

    DWORD  CurrentOffset;
    PVOID  LineReadContext = NULL;
    BOOL   Result;
    YORI_STRING LineString;
    YORI_STRING Arg;
    PYORI_STRING UserFileName = NULL;
    YORI_STRING FileName;
    YORI_LIB_VT_CALLBACK_FUNCTIONS Callbacks;
    DWORD  StartArg = 0;

    BOOLEAN StreamStarted = FALSE;
    BOOLEAN ExecMode = FALSE;
    BOOLEAN DisplayUsage = FALSE;
    BOOLEAN ArgParsed = FALSE;
    BOOLEAN StripEscapes = FALSE;

    YORI_LIB_LINE_ENDING LineEnding;
    BOOL TimeoutReached;

    Callbacks.Context = 0;
    CvtvtHtml4SetFunctions(&Callbacks);

    //
    //  Parse arguments
    //

    for (CurrentOffset = 1; CurrentOffset < ArgC; CurrentOffset++) {
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[CurrentOffset]));
        if (YoriLibIsCommandLineOption(&ArgV[CurrentOffset], &Arg)) {

            ArgParsed = FALSE;

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                ArgParsed = TRUE;
                DisplayUsage = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2015-2018"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("exec")) == 0) {
                ArgParsed = TRUE;
                ExecMode = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("html4")) == 0) {
                ArgParsed = TRUE;
                StripEscapes = FALSE;
                CvtvtHtml4SetFunctions(&Callbacks);
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("html5")) == 0) {
                ArgParsed = TRUE;
                StripEscapes = FALSE;
                CvtvtHtml5SetFunctions(&Callbacks);
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("rtf")) == 0) {
                ArgParsed = TRUE;
                StripEscapes = FALSE;
                CvtvtRtfSetFunctions(&Callbacks);
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("text")) == 0) {
                ArgParsed = TRUE;
                StripEscapes = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("win32")) == 0) {
                ArgParsed = TRUE;
                StripEscapes = FALSE;
                YoriLibConsoleSetFunctions(&Callbacks);
            }

            if (!ArgParsed) {
                DisplayUsage = TRUE;
            }

        } else {
            StartArg = CurrentOffset;
            break;
        }
    }

    YoriLibInitEmptyString(&FileName);

    //
    //  As a bit of a hack, if the input is stdin and stdin is to
    //  a console (as opposed to a pipe or file) assume the user isn't
    //  sure how to run this program and help them along.
    //

    if (StartArg == 0) {
        if (YoriLibIsStdInConsole()) {
            DisplayUsage = TRUE;
        }
        UserFileName = NULL;
    } else {
        UserFileName = &ArgV[StartArg];
    }

    if (DisplayUsage) {
        CvtvtUsage();
        return EXIT_FAILURE;
    }

    //
    //  Attempt to enable backup privilege so an administrator can access more
    //  objects successfully.
    //

    YoriLibEnableBackupPrivilege();

    //
    //  If we have a file name, read it; otherwise read from stdin
    //

    if (ExecMode) {
        if (UserFileName == NULL) {
            CvtvtUsage();
            return EXIT_FAILURE;
        } else {
            YORI_STRING CmdLine;
            DWORD Err;

            YoriLibInitEmptyString(&CmdLine);
            if (!YoriLibBuildCmdlineFromArgcArgv(ArgC - StartArg, &ArgV[StartArg], TRUE, TRUE, &CmdLine)) {
                return EXIT_FAILURE;
            }
            Err = CvtvtLaunchChildProcess(&ArgV[StartArg], &CmdLine, &hSource, &hControl);
            YoriLibFreeStringContents(&CmdLine);
            if (Err != ERROR_SUCCESS) {
                LPTSTR ErrText = YoriLibGetWinErrorText(Err);
                YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("cvtvt: could not launch process: %s"), ErrText);
                YoriLibFreeWinErrorText(ErrText);
                return EXIT_FAILURE;
            }
            InputPumpThread = CreateThread(NULL, 0, CvtvtInputPumpThread, hControl, 0, NULL);
        }
    } else if (UserFileName != NULL) {
        if (!YoriLibUserStringToSingleFilePath(UserFileName, TRUE, &FileName)) {
            return EXIT_FAILURE;
        }

        hSource = CreateFile(FileName.StartOfString,
                             GENERIC_READ,
                             FILE_SHARE_READ|FILE_SHARE_DELETE,
                             NULL,
                             OPEN_EXISTING,
                             FILE_ATTRIBUTE_NORMAL|FILE_FLAG_BACKUP_SEMANTICS,
                             NULL);
        if (hSource == INVALID_HANDLE_VALUE) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("cvtvt: could not open file, error %i\n"), (int)GetLastError());
            return EXIT_FAILURE;
        }
    } else {
        hSource = GetStdHandle(STD_INPUT_HANDLE);
    }

    hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    if (StripEscapes) {
        DWORD CurrentMode;
        if (GetConsoleMode(hOutput, &CurrentMode)) {
            YoriLibConsoleNoEscapeSetFunctions(&Callbacks);
        } else {
            YoriLibUtf8TextNoEscapesSetFunctions(&Callbacks);
        }
    }

    YoriLibInitEmptyString(&LineString);

    Result = TRUE;

    while (YoriLibReadLineToStringEx(&LineString, &LineReadContext, TRUE, 100, hSource, &LineEnding, &TimeoutReached) || TimeoutReached) {

        //
        //  Start producing HTML
        //

        if (!StreamStarted) {
            Callbacks.InitializeStream(hOutput, &Callbacks.Context);
            StreamStarted = TRUE;
        }

        if (LineString.LengthInChars > 0) {

            if (!YoriLibProcessVtEscapesOnOpenStream(LineString.StartOfString,
                                                     LineString.LengthInChars,
                                                     hOutput,
                                                     &Callbacks)) {

                Result = FALSE;
                break;
            }

        }

        if (LineEnding != YoriLibLineEndingNone) {
            if (!YoriLibProcessVtEscapesOnOpenStream(_T("\n"),
                                                     1,
                                                     hOutput,
                                                     &Callbacks)) {

                Result = FALSE;
                break;
            }
        }
    }

    if (StreamStarted) {
        Callbacks.EndStream(hOutput, &Callbacks.Context);
    }

    YoriLibLineReadCloseOrCache(LineReadContext);
    YoriLibFreeStringContents(&LineString);
    if (hSource != INVALID_HANDLE_VALUE && UserFileName != NULL) {
        CloseHandle(hSource);
    }

    if (InputPumpThread != NULL) {
        //
        //  TerminateThread is evil because the thread can't do any cleanup.
        //  Here we "know" the thread is very simple, holds no locks, does
        //  no cleanup of its own, etc.
        //
#if defined(_MSC_VER) && (_MSC_VER >= 1500)
#pragma warning(disable: 6258)
#endif
        TerminateThread(InputPumpThread, 0);
        CloseHandle(InputPumpThread);
    }

    if (hControl != INVALID_HANDLE_VALUE) {
        CloseHandle(hControl);
    }

    YoriLibFreeStringContents(&FileName);

#if !YORI_BUILTIN
    YoriLibLineReadCleanupCache();
#endif

    if (Result) {
        return EXIT_SUCCESS;
    } else {
        return EXIT_FAILURE;
    }
}

// vim:sw=4:ts=4:et:


<|start_filename|>pause/pause.c<|end_filename|>
/**
 * @file pause/pause.c
 *
 * Yori shell wait for the user to press a key
 *
 * Copyright (c) 2017-2018 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, PAUSEESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 Help text to display to the user.
 */
const
CHAR strPauseHelpText[] =
        "\n"
        "Prompt the user to press any key before continuing.\n"
        "\n"
        "PAUSE [-license]\n";

/**
 Display usage text to the user.
 */
BOOL
PauseHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Pause %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strPauseHelpText);
    return TRUE;
}

#ifdef YORI_BUILTIN
/**
 The main entrypoint for the pause builtin command.
 */
#define ENTRYPOINT YoriCmd_YPAUSE
#else
/**
 The main entrypoint for the pause standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the pause cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process indicating success or failure.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    TCHAR Char;
    DWORD BytesRead;
    DWORD i;
    YORI_STRING Arg;

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                PauseHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2017-2018"));
                return EXIT_SUCCESS;
            }
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Press any key to continue...\n"));
    if (!SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), 0)) {
        if (!ReadFile(GetStdHandle(STD_INPUT_HANDLE), &Char, 1, &BytesRead, NULL)) {
            return EXIT_FAILURE;
        }
    } else {
        INPUT_RECORD InputRecord;

        while(TRUE) {
            if (!ReadConsoleInput(GetStdHandle(STD_INPUT_HANDLE), &InputRecord, 1, &BytesRead)) {
                break;
            }

            if (InputRecord.EventType == KEY_EVENT &&
                InputRecord.Event.KeyEvent.bKeyDown) {
                break;
            }
        }
    }

    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>lib/process.c<|end_filename|>
/**
 * @file lib/process.c
 *
 * Yori process enumeration support routines
 *
 * Copyright (c) 2018-2020 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "yoripch.h"
#include "yorilib.h"

/**
 Load information about all processes currently executing in the system.

 @param ProcessInfo On successful completion, updated to point to a list of
        processes executing within the system.  The caller is expected to
        free this with YoriLibFree.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOL
YoriLibGetSystemProcessList(
    __out PYORI_SYSTEM_PROCESS_INFORMATION *ProcessInfo
    )
{
    PYORI_SYSTEM_PROCESS_INFORMATION LocalProcessInfo = NULL;
    DWORD BytesReturned;
    DWORD BytesAllocated;
    LONG Status;

    if (DllNtDll.pNtQuerySystemInformation == NULL) {
        return FALSE;
    }

    BytesAllocated = 0;

    do {

        if (LocalProcessInfo != NULL) {
            YoriLibFree(LocalProcessInfo);
        }

        if (BytesAllocated == 0) {
            BytesAllocated = 64 * 1024;
        } else if (BytesAllocated <= 16 * 1024 * 1024) {
            BytesAllocated = BytesAllocated * 4;
        } else {
            return FALSE;
        }

        LocalProcessInfo = YoriLibMalloc(BytesAllocated);
        if (LocalProcessInfo == NULL) {
            return FALSE;
        }

        Status = DllNtDll.pNtQuerySystemInformation(SystemProcessInformation, LocalProcessInfo, BytesAllocated, &BytesReturned);
    } while (Status == (LONG)0xc0000004);


    if (Status != 0) {
        YoriLibFree(LocalProcessInfo);
        return FALSE;
    }

    if (BytesReturned == 0) {
        YoriLibFree(LocalProcessInfo);
        return FALSE;
    }

    *ProcessInfo = LocalProcessInfo;
    return TRUE;
}

/**
 Load information about all handles currently open in the system.

 @param HandlesInfo On successful completion, updated to point to a list of
        handles open within the system.  The caller is expected to free this
        with YoriLibFree.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOL
YoriLibGetSystemHandlesList(
    __out PYORI_SYSTEM_HANDLE_INFORMATION_EX *HandlesInfo
    )
{
    PYORI_SYSTEM_HANDLE_INFORMATION_EX LocalHandlesInfo = NULL;
    DWORD BytesReturned;
    DWORD BytesAllocated;
    LONG Status;

    if (DllNtDll.pNtQuerySystemInformation == NULL) {
        return FALSE;
    }

    BytesAllocated = 0;

    do {

        if (LocalHandlesInfo != NULL) {
            YoriLibFree(LocalHandlesInfo);
        }

        if (BytesAllocated == 0) {
            BytesAllocated = 64 * 1024;
        } else if (BytesAllocated <= 16 * 1024 * 1024) {
            BytesAllocated = BytesAllocated * 4;
        } else {
            return FALSE;
        }

        LocalHandlesInfo = YoriLibMalloc(BytesAllocated);
        if (LocalHandlesInfo == NULL) {
            return FALSE;
        }

        Status = DllNtDll.pNtQuerySystemInformation(SystemExtendedHandleInformation, LocalHandlesInfo, BytesAllocated, &BytesReturned);
    } while (Status == (LONG)0xc0000004);


    if (Status != 0) {
        YoriLibFree(LocalHandlesInfo);
        return FALSE;
    }

    if (BytesReturned == 0) {
        YoriLibFree(LocalHandlesInfo);
        return FALSE;
    }

    *HandlesInfo = LocalHandlesInfo;
    return TRUE;
}

// vim:sw=4:ts=4:et:


<|start_filename|>intcmp/intcmp.c<|end_filename|>
/**
 * @file intcmp/intcmp.c
 *
 * Yori shell compare integers
 *
 * Copyright (c) 2018 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 Help text to display to the user.
 */
const
CHAR strIntCmpHelpText[] =
        "\n"
        "Compare two integer values.\n"
        "\n"
        "INTCMP [-license] [--] <string><operator><string>\n"
        "\n"
        "   --             Treat all further arguments as comparison parameters\n"
        "\n"
        "Operators are:\n"
        "   ==             Numbers match exactly\n"
        "   !=             Numbers do not match\n"
        "   >=             First number greater than or equal to second number\n"
        "   <=             First number less than or equal to second number\n"
        "   >              First number greater than second number\n"
        "   <              First number less than second number\n";

/**
 Display usage text to the user.
 */
BOOL
IntCmpHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("IntCmp %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strIntCmpHelpText);
    return TRUE;
}

/**
 An array index for the exact match operator.
 */
#define INTCMP_OPERATOR_EXACT_MATCH      0

/**
 An array index for the no match operator.
 */
#define INTCMP_OPERATOR_NO_MATCH         1

/**
 An array index for the greater than or equal operator.
 */
#define INTCMP_OPERATOR_GREATER_OR_EQUAL 2

/**
 An array index for the less than or equal operator.
 */
#define INTCMP_OPERATOR_LESS_OR_EQUAL    3

/**
 An array index for the greater than operator.
 */
#define INTCMP_OPERATOR_GREATER          4

/**
 An array index for the less than operator.
 */
#define INTCMP_OPERATOR_LESS             5

/**
 The total number of array elements present.
 */
#define INTCMP_OPERATOR_BEYOND_MAX       6

#ifdef YORI_BUILTIN
/**
 The main entrypoint for the intcmp builtin command.
 */
#define ENTRYPOINT YoriCmd_INTCMP
#else
/**
 The main entrypoint for the intcmp standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the intcmp cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the child process on success, or failure if the child
         could not be launched.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD StartArg = 0;
    DWORD i;
    YORI_STRING EntireExpression;
    YORI_STRING FirstPart;
    YORI_STRING SecondPart;
    YORI_STRING OperatorMatches[INTCMP_OPERATOR_BEYOND_MAX];
    YORI_STRING Arg;
    PYORI_STRING MatchingOperator;
    DWORD OperatorIndex;
    DWORD CharsConsumed;
    LONGLONG FirstNumber;
    LONGLONG SecondNumber;
    int Result;

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                IntCmpHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2018"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                if (i + 1 < ArgC) {
                    ArgumentUnderstood = TRUE;
                    StartArg = i + 1;
                    break;
                }
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    if (StartArg == 0) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("intcmp: missing argument\n"));
        return EXIT_FAILURE;
    }

    if (!YoriLibBuildCmdlineFromArgcArgv(ArgC - StartArg, &ArgV[StartArg], TRUE, FALSE, &EntireExpression)) {
        return EXIT_FAILURE;
    }

    YoriLibConstantString(&OperatorMatches[INTCMP_OPERATOR_EXACT_MATCH], _T("=="));
    YoriLibConstantString(&OperatorMatches[INTCMP_OPERATOR_NO_MATCH], _T("!="));
    YoriLibConstantString(&OperatorMatches[INTCMP_OPERATOR_GREATER_OR_EQUAL], _T(">="));
    YoriLibConstantString(&OperatorMatches[INTCMP_OPERATOR_LESS_OR_EQUAL], _T("<="));
    YoriLibConstantString(&OperatorMatches[INTCMP_OPERATOR_GREATER], _T(">"));
    YoriLibConstantString(&OperatorMatches[INTCMP_OPERATOR_LESS], _T("<"));

    MatchingOperator = YoriLibFindFirstMatchingSubstring(&EntireExpression, sizeof(OperatorMatches)/sizeof(OperatorMatches[0]), OperatorMatches, &OperatorIndex);
    if (MatchingOperator == NULL) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("intcmp: missing operator\n"));
        YoriLibFreeStringContents(&EntireExpression);
        return EXIT_FAILURE;
    }

    YoriLibInitEmptyString(&FirstPart);

    FirstPart.StartOfString = EntireExpression.StartOfString;
    FirstPart.LengthInChars = OperatorIndex;

    YoriLibInitEmptyString(&SecondPart);

    SecondPart.StartOfString = EntireExpression.StartOfString + OperatorIndex + MatchingOperator->LengthInChars;
    SecondPart.LengthInChars = EntireExpression.LengthInChars - OperatorIndex - MatchingOperator->LengthInChars;

    YoriLibTrimSpaces(&FirstPart);
    YoriLibTrimSpaces(&SecondPart);

    if (!YoriLibStringToNumber(&FirstPart, TRUE, &FirstNumber, &CharsConsumed) || CharsConsumed == 0) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("intcmp: non numeric argument\n"));
        YoriLibFreeStringContents(&EntireExpression);
        return EXIT_FAILURE;
    }

    if (!YoriLibStringToNumber(&SecondPart, TRUE, &SecondNumber, &CharsConsumed) || CharsConsumed == 0) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("intcmp: non numeric argument\n"));
        YoriLibFreeStringContents(&EntireExpression);
        return EXIT_FAILURE;
    }

    YoriLibFreeStringContents(&EntireExpression);

    Result = EXIT_FAILURE;

    if (MatchingOperator == &OperatorMatches[INTCMP_OPERATOR_EXACT_MATCH]) {

        if (FirstNumber == SecondNumber) {
            Result = EXIT_SUCCESS;
        } else {
            Result = EXIT_FAILURE;
        }

    } else if (MatchingOperator == &OperatorMatches[INTCMP_OPERATOR_NO_MATCH]) {

        if (FirstNumber != SecondNumber) {
            Result = EXIT_SUCCESS;
        } else {
            Result = EXIT_FAILURE;
        }

    } else if (MatchingOperator == &OperatorMatches[INTCMP_OPERATOR_GREATER_OR_EQUAL]) {

        if (FirstNumber >= SecondNumber) {
            Result = EXIT_SUCCESS;
        } else {
            Result = EXIT_FAILURE;
        }

    } else if (MatchingOperator == &OperatorMatches[INTCMP_OPERATOR_LESS_OR_EQUAL]) {

        if (FirstNumber <= SecondNumber) {
            Result = EXIT_SUCCESS;
        } else {
            Result = EXIT_FAILURE;
        }

    } else if (MatchingOperator == &OperatorMatches[INTCMP_OPERATOR_GREATER]) {

        if (FirstNumber > SecondNumber) {
            Result = EXIT_SUCCESS;
        } else {
            Result = EXIT_FAILURE;
        }

    } else if (MatchingOperator == &OperatorMatches[INTCMP_OPERATOR_LESS]) {

        if (FirstNumber < SecondNumber) {
            Result = EXIT_SUCCESS;
        } else {
            Result = EXIT_FAILURE;
        }

    } else {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("intcmp: operator not implemented %x\n"), MatchingOperator);
        return EXIT_FAILURE;
    }

    return Result;
}

// vim:sw=4:ts=4:et:


<|start_filename|>ypm/ypminst.c<|end_filename|>
/**
 * @file ypm/ypminst.c
 *
 * Yori shell package manager install packages
 *
 * Copyright (c) 2018-2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>
#include <yoripkg.h>
#include "ypm.h"

/**
 Help text to display to the user.
 */
const
CHAR strYpmInstallHelpText[] =
        "\n"
        "Installs one or more packages.\n"
        "\n"
        "YPM [-license]\n"
        "YPM -i <file> ...\n";

/**
 Help text to display to the user.
 */
const
CHAR strYpmInstallRemoteHelpText[] =
        "\n"
        "Install packages from remote servers.\n"
        "\n"
        "YPM [-license]\n"
        "YPM -ri [-a <arch>] [-v <version>] <pkgname>\n"
        "\n"
        "   -a             Specify a CPU architecture to install\n"
        "   -v             Specify a package version to install\n";

/**
 Help text to display to the user.
 */
const
CHAR strYpmInstallSourceHelpText[] =
        "\n"
        "Installs source for one or more packages.\n"
        "\n"
        "YPM [-license]\n"
        "YPM -src [<pkg>]\n";

/**
 Help text to display to the user.
 */
const
CHAR strYpmInstallSymbolsHelpText[] =
        "\n"
        "Installs debugging symbols for one or more packages.\n"
        "\n"
        "YPM [-license]\n"
        "YPM -sym [<pkg>]\n";

/**
 Display usage text to the user.
 */
BOOL
YpmInstallHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Ypm %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strYpmInstallHelpText);
    return TRUE;
}

/**
 Display usage text to the user.
 */
BOOL
YpmInstallRemoteHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Ypm %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strYpmInstallRemoteHelpText);
    return TRUE;
}

/**
 Display usage text to the user.
 */
BOOL
YpmInstallSourceHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Ypm %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strYpmInstallSourceHelpText);
    return TRUE;
}

/**
 Display usage text to the user.
 */
BOOL
YpmInstallSymbolsHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Ypm %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strYpmInstallSymbolsHelpText);
    return TRUE;
}

/**
 Install one or more packages.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process.
 */
DWORD
YpmInstall(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    YORI_STRING Arg;

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                YpmInstallHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2017-2021"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                ArgumentUnderstood = TRUE;
                StartArg = i + 1;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    if (StartArg == 0 || StartArg >= ArgC) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("ypm: missing argument\n"));
        return EXIT_FAILURE;
    }

    for (i = StartArg; i < ArgC; i++) {
        YoriPkgInstallSinglePackage(&ArgV[i], NULL);
    }

    return EXIT_SUCCESS;
}

/**
 Install the source for one or more packages.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process.
 */
DWORD
YpmInstallSource(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    YORI_STRING Arg;

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                YpmInstallSourceHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2017-2021"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                ArgumentUnderstood = TRUE;
                StartArg = i + 1;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    if (StartArg == 0 || StartArg >= ArgC) {
        YoriPkgInstallSourceForInstalledPackages();
    } else {
        for (i = StartArg; i < ArgC; i++) {
            YoriPkgInstallSourceForSinglePackage(&ArgV[i]);
        }
    }

    return EXIT_SUCCESS;
}

/**
 Install the debugging symbols for one or more packages.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process.
 */
DWORD
YpmInstallSymbols(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    YORI_STRING Arg;

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                YpmInstallSymbolsHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2017-2021"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                ArgumentUnderstood = TRUE;
                StartArg = i + 1;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    if (StartArg == 0 || StartArg >= ArgC) {
        YoriPkgInstallSymbolsForInstalledPackages();
    } else {
        for (i = StartArg; i < ArgC; i++) {
            YoriPkgInstallSymbolForSinglePackage(&ArgV[i]);
        }
    }

    return EXIT_SUCCESS;
}

/**
 Install packages from remote sources.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process.
 */
DWORD
YpmInstallRemote(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    YORI_STRING Arg;
    PYORI_STRING NewVersion = NULL;
    PYORI_STRING NewArch = NULL;
    DWORD PkgCount;

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                YpmInstallRemoteHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2017-2021"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("a")) == 0) {
                if (i + 1 < ArgC) {
                    NewArch = &ArgV[i + 1];
                    i++;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("v")) == 0) {
                if (i + 1 < ArgC) {
                    NewVersion = &ArgV[i + 1];
                    i++;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                ArgumentUnderstood = TRUE;
                StartArg = i + 1;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    PkgCount = ArgC - StartArg;
    if (StartArg == 0 || PkgCount == 0) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("ypm: missing package name\n"));
        return EXIT_FAILURE;
    }
    if (!YoriPkgInstallRemotePackages(&ArgV[StartArg], PkgCount, NULL, NULL, NewVersion, NewArch)) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("ypm: one or more packages could not be found or installed\n"));
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>lib/printf.c<|end_filename|>
/**
 * @file printf.c
 *
 * Unicode versions of printf functions.
 *
 * Copyright (c) 2014 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

/**
 Indicate that the printf routine should generate YoriLibVSPrintf as a 
 Unicode native form.
 */
#define UNICODE 1

/**
 Indicate that the printf routine should generate YoriLibVSPrintf as a 
 Unicode native form.
 */
#define _UNICODE 1

#include "yoripch.h"
#include "yorilib.h"

#include "printf.inc"

/**
 Indicate that the printf routine should generate YoriLibVSPrintfSize.
 */
#define PRINTF_SIZEONLY 1
#include "printf.inc"

/**
 Process a printf format string and output the result into a NULL terminated
 buffer of specified size.

 @param szDest The buffer to populate with the result.

 @param len The number of characters in the buffer.

 @param szFmt The format string to process.

 @return The number of characters successfully populated into the buffer, or
         -1 on error.
 */
int
YoriLibSPrintfS(
    __out_ecount(len) LPTSTR szDest,
    __in DWORD len,
    __in LPCTSTR szFmt,
    ...
    )
{
    va_list marker;
    int out_len;

    va_start( marker, szFmt );
    out_len = YoriLibVSPrintf(szDest, len, szFmt, marker);
    va_end( marker );
    return out_len;
}

#if defined(_MSC_VER) && (_MSC_VER >= 1500)
#pragma warning(push)
#pragma warning(disable: 6386) // Obviously this is the buffer unsafe version
#endif

/**
 Process a printf format string and output the result into a NULL terminated
 buffer which is assumed to be large enough to hold the result.

 @param szDest The buffer to populate with the result.

 @param szFmt The format string to process.

 @return The number of characters successfully populated into the buffer, or
         -1 on error.
 */
int
YoriLibSPrintf(
    __out LPTSTR szDest,
    __in LPCTSTR szFmt,
    ...
    )
{
    va_list marker;
    int out_len;

    va_start( marker, szFmt );
    out_len = YoriLibVSPrintf(szDest, (DWORD)-1, szFmt, marker);
    va_end( marker );
    return out_len;
}

#if defined(_MSC_VER) && (_MSC_VER >= 1500)
#pragma warning(pop)
#endif

/**
 Process a printf format string and output the result into a Yori string.
 If the string is not large enough to contain the result, it is reallocated
 internally.

 @param Dest The string to populate with the result.

 @param szFmt The format string to process.

 @param marker The arguments that are specific to the format string.

 @return The number of characters successfully populated into the buffer, or
         -1 on error.
 */
__success(return >= 0)
int
YoriLibYPrintfInternal(
    __inout PYORI_STRING Dest,
    __in LPCTSTR szFmt,
    __in va_list marker
    )
{
    int required_len;
    int out_len;

    required_len = YoriLibVSPrintfSize(szFmt, marker);
    if (required_len < 0) {
        return required_len;
    }
    if ((DWORD)required_len > Dest->LengthAllocated) {
        YoriLibFreeStringContents(Dest);

        Dest->MemoryToFree = YoriLibReferencedMalloc(required_len * sizeof(TCHAR));
        if (Dest->MemoryToFree == NULL) {
            return -1;
        }

        Dest->StartOfString = Dest->MemoryToFree;
        Dest->LengthAllocated = required_len;
    }

    out_len = YoriLibVSPrintf(Dest->StartOfString, Dest->LengthAllocated, szFmt, marker);
    if (out_len >= 0) {
        Dest->LengthInChars = out_len;
    }
    return out_len;
}

/**
 Process a printf format string and output the result into a Yori string.
 If the string is not large enough to contain the result, it is reallocated
 internally.

 @param Dest The string to populate with the result.

 @param szFmt The format string to process.

 @return The number of characters successfully populated into the buffer, or
         -1 on error.
 */
__success(return >= 0)
int
YoriLibYPrintf(
    __inout PYORI_STRING Dest,
    __in LPCTSTR szFmt,
    ...
    )
{
    va_list marker;
    int out_len;

    va_start( marker, szFmt );
    out_len = YoriLibYPrintfInternal(Dest, szFmt, marker);
    va_end( marker );
    return out_len;
}

/**
 Process a printf format string and count the number of characters required
 to contain the result, including the NULL terminator character.

 @param szFmt The format string to process.

 @return The number of characters that could be populated into the buffer, or
         -1 on error.
 */
int
YoriLibSPrintfSize(
    __in LPCTSTR szFmt,
    ...
    )
{
    va_list marker;
    int out_len;

    va_start( marker, szFmt );
    out_len = YoriLibVSPrintfSize(szFmt, marker);
    va_end( marker );
    return out_len;
}

// vim:sw=4:ts=4:et:


<|start_filename|>more/Makefile<|end_filename|>

BINARIES=ymore.exe

!INCLUDE "..\config\common.mk"

!IF $(PDB)==1
LINKPDB=/Pdb:ymore.pdb
!ENDIF

BIN_OBJS=\
	 ingest.obj       \
	 moreinit.obj     \
	 more.obj         \
	 viewport.obj     \

MOD_OBJS=\
	 ingest.obj       \
	 moreinit.obj     \
	 mod_more.obj     \
	 viewport.obj     \

compile: $(BIN_OBJS) builtins.lib

ymore.exe: $(BIN_OBJS) $(YORILIB) $(YORICRT) $(YORIVER)
	@echo $@
	@$(LINK) $(LDFLAGS) -entry:$(YENTRY) $(BIN_OBJS) $(LIBS) $(CRTLIB) $(YORILIB) $(YORIVER) -version:$(YORI_VER_MAJOR).$(YORI_VER_MINOR) $(LINKPDB) -out:$@

mod_more.obj: more.c
	@echo $@
	@$(CC) -c -DYORI_BUILTIN=1 $(CFLAGS) -Fo$@ more.c

builtins.lib: $(MOD_OBJS)
	@echo $@
	@$(LIB32) $(LIBFLAGS) $(MOD_OBJS) -out:$@


<|start_filename|>lib/Makefile<|end_filename|>

compile: yorilib.lib yoriver.obj

!INCLUDE "..\config\common.mk"

OBJS=\
	 builtin.obj  \
	 cabinet.obj  \
	 call.obj     \
	 cancel.obj   \
	 clip.obj     \
	 cmdline.obj  \
	 color.obj    \
	 cshot.obj    \
	 curdir.obj   \
	 cvthtml.obj  \
	 cvtrtf.obj   \
	 debug.obj    \
	 dyld.obj     \
	 dyld_adv.obj \
	 dyld_cab.obj \
	 dyld_net.obj \
	 env.obj      \
	 ep_yori.obj  \
	 filecomp.obj \
	 fileenum.obj \
	 filefilt.obj \
	 fileinfo.obj \
	 fullpath.obj \
	 group.obj    \
	 hash.obj     \
	 hexdump.obj  \
	 iconv.obj    \
	 jobobj.obj   \
	 license.obj  \
	 lineread.obj \
	 list.obj     \
	 malloc.obj   \
	 movefile.obj \
	 numkey.obj   \
	 osver.obj    \
	 path.obj     \
	 printf.obj   \
	 printfa.obj  \
	 priv.obj     \
	 process.obj  \
	 progman.obj  \
	 recycle.obj  \
	 scut.obj     \
	 select.obj   \
	 string.obj   \
	 strmenum.obj \
	 temp.obj     \
	 update.obj   \
	 util.obj     \
	 vt.obj       \

all: yorilib.lib yoriver.obj

yorilib.lib: $(OBJS)
	@echo $@
	@$(LIB32) $(LIBFLAGS) $(OBJS) /out:yorilib.lib

yoriver.obj: yoriver.inc
	@echo $(**F)
	@if exist $@ erase $@
	@$(RC) /fo$(@B).res $(RCFLAGS) /d YORI_VER_MAJOR=$(YORI_VER_MAJOR) /d YORI_VER_MINOR=$(YORI_VER_MINOR) /d YORI_BIN_VER_MAJOR=$(YORI_VER_MAJOR) /d YORI_BIN_VER_MINOR=$(YORI_VER_MINOR) $** >NUL
	@if not exist $@ ren $(@B).res $@



<|start_filename|>finfo/finfo.c<|end_filename|>
/**
 * @file finfo/finfo.c
 *
 * Yori shell display file metadata
 *
 * Copyright (c) 2018-2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 Help text to display to the user.
 */
const
CHAR strFInfoHelpText[] =
        "\n"
        "Output information about file metadata.\n"
        "\n"
        "FINFO [-license] [-b] [-d] [-f fmt] [-s] <file>...\n"
        "\n"
        "   -b             Use basic search criteria for files only\n"
        "   -d             Return directories rather than directory contents\n"
        "   -f             Specify a custom format string\n"
        "   -s             Process files from all subdirectories\n";

/**
 Context passed to the callback which is invoked for each file found.
 */
typedef struct _FINFO_CONTEXT {

    /**
     The format string to apply when deciding which variables associated
     with the file to output and the form with which to output them.
     */
    YORI_STRING FormatString;

    /**
     Pointer to the full file path for a matching file.
     */
    PYORI_STRING FilePath;

    /**
     Pointer to the directory information from the matching file.
     */
    PWIN32_FIND_DATA FileInfo;

    /**
     Extracted information about the file.
     */
    YORI_FILE_INFO Entry;

    /**
     Records the total number of files processed.
     */
    LONGLONG FilesFound;

    /**
     Records the total number of files processed for each argument processed.
     */
    LONGLONG FilesFoundThisArg;

} FINFO_CONTEXT, *PFINFO_CONTEXT;

/**
 Specifies a pointer to a function which can collect file information from
 the disk or file system for some particular piece of data.
 */
typedef BOOL (* PFINFO_COLLECT_FN)(PYORI_FILE_INFO, PWIN32_FIND_DATA, PYORI_STRING);

/**
 Specifies a pointer to a function which can output a particular piece of file
 information.
 */
typedef DWORD (* PFINFO_OUTPUT_FN)(PFINFO_CONTEXT, PYORI_STRING);

/**
 Output a 64 bit integer.

 @param LargeInt A large integer to output.

 @param NumberBase Specifies the numeric base to use.  Should be 10 for
        decimal or 16 for hex.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputLargeInteger(
    __in LARGE_INTEGER LargeInt,
    __in DWORD NumberBase,
    __inout PYORI_STRING OutputString
    )
{
    YORI_STRING String;
    TCHAR StringBuffer[32];

    YoriLibInitEmptyString(&String);
    String.StartOfString = StringBuffer;
    String.LengthAllocated = sizeof(StringBuffer)/sizeof(StringBuffer[0]);

    YoriLibNumberToString(&String, LargeInt.QuadPart, NumberBase, 0, ' ');

    if (OutputString->LengthAllocated >= String.LengthInChars) {
        memcpy(OutputString->StartOfString, String.StartOfString, String.LengthInChars * sizeof(TCHAR));
    }

    return String.LengthInChars;
}

/**
 Output the access date year.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputAccessDateYear(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    if (OutputString->LengthAllocated >= 4) {
        YoriLibYPrintf(OutputString, _T("%04i"), Context->Entry.AccessTime.wYear);
    }
    return 4;
}

/**
 Output the access date month.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputAccessDateMon(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    if (OutputString->LengthAllocated >= 2) {
        YoriLibYPrintf(OutputString, _T("%02i"), Context->Entry.AccessTime.wMonth);
    }
    return 2;
}

/**
 Output the access date day.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputAccessDateDay(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    if (OutputString->LengthAllocated >= 2) {
        YoriLibYPrintf(OutputString, _T("%02i"), Context->Entry.AccessTime.wDay);
    }
    return 2;
}

/**
 Output the access time hour.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputAccessTimeHour(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    if (OutputString->LengthAllocated >= 2) {
        YoriLibYPrintf(OutputString, _T("%02i"), Context->Entry.AccessTime.wHour);
    }
    return 2;
}

/**
 Output the access time minute.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputAccessTimeMin(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    if (OutputString->LengthAllocated >= 2) {
        YoriLibYPrintf(OutputString, _T("%02i"), Context->Entry.AccessTime.wMinute);
    }
    return 2;
}

/**
 Output the access time second.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputAccessTimeSec(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    if (OutputString->LengthAllocated >= 2) {
        YoriLibYPrintf(OutputString, _T("%02i"), Context->Entry.AccessTime.wSecond);
    }
    return 2;
}


/**
 Output the allocated range count.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputAllocatedRangeCount(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    return FInfoOutputLargeInteger(Context->Entry.AllocatedRangeCount, 10, OutputString);
}

/**
 Output the allocation size.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputAllocationSize(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    return FInfoOutputLargeInteger(Context->Entry.AllocationSize, 10, OutputString);
}

/**
 Output the allocation size in hex.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputAllocationSizeHex(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    return FInfoOutputLargeInteger(Context->Entry.AllocationSize, 16, OutputString);
}

/**
 Output the case sensitivity state.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputCaseSensitivity(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    if (OutputString->LengthAllocated >= 1) {
        YoriLibYPrintf(OutputString, _T("%01i"), Context->Entry.CaseSensitive);
    }
    return 1;
}

/**
 Output the compressed size.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputCompressedSize(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    return FInfoOutputLargeInteger(Context->Entry.CompressedFileSize, 10, OutputString);
}

/**
 Output the compressed size in hex.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputCompressedSizeHex(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    return FInfoOutputLargeInteger(Context->Entry.CompressedFileSize, 16, OutputString);
}

/**
 Output the create date year.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputCreateDateYear(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    if (OutputString->LengthAllocated >= 4) {
        YoriLibYPrintf(OutputString, _T("%04i"), Context->Entry.CreateTime.wYear);
    }
    return 4;
}

/**
 Output the create date month.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputCreateDateMon(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    if (OutputString->LengthAllocated >= 2) {
        YoriLibYPrintf(OutputString, _T("%02i"), Context->Entry.CreateTime.wMonth);
    }
    return 2;
}

/**
 Output the create date day.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputCreateDateDay(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    if (OutputString->LengthAllocated >= 2) {
        YoriLibYPrintf(OutputString, _T("%02i"), Context->Entry.CreateTime.wDay);
    }
    return 2;
}

/**
 Output the create time hour.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputCreateTimeHour(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    if (OutputString->LengthAllocated >= 2) {
        YoriLibYPrintf(OutputString, _T("%02i"), Context->Entry.CreateTime.wHour);
    }
    return 2;
}

/**
 Output the create time minute.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputCreateTimeMin(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    if (OutputString->LengthAllocated >= 2) {
        YoriLibYPrintf(OutputString, _T("%02i"), Context->Entry.CreateTime.wMinute);
    }
    return 2;
}

/**
 Output the create time second.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputCreateTimeSec(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    if (OutputString->LengthAllocated >= 2) {
        YoriLibYPrintf(OutputString, _T("%02i"), Context->Entry.CreateTime.wSecond);
    }
    return 2;
}

/**
 Output the file description from the version resource.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputDescription(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    DWORD DescriptionLength = _tcslen(Context->Entry.Description);
    if (OutputString->LengthAllocated >= DescriptionLength) {
        memcpy(OutputString->StartOfString, Context->Entry.Description, DescriptionLength * sizeof(TCHAR));
        OutputString->LengthInChars = DescriptionLength;
    }
    return DescriptionLength;
}

/**
 Output the file extension.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputExt(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    DWORD ExtLen = 0;

    if (Context->Entry.Extension != NULL) {
        ExtLen = _tcslen(Context->Entry.Extension);

        if (OutputString->LengthAllocated >= ExtLen) {
            memcpy(OutputString->StartOfString, Context->Entry.Extension, ExtLen * sizeof(TCHAR));
        }
    }

    return ExtLen;
}

/**
 Output the file ID.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputFileId(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    return FInfoOutputLargeInteger(Context->Entry.FileId, 10, OutputString);
}

/**
 Output the file ID in hex.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputFileIdHex(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    return FInfoOutputLargeInteger(Context->Entry.FileId, 16, OutputString);
}

/**
 Output the file version string.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputFileVersionString(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    DWORD FileVersionLength = _tcslen(Context->Entry.FileVersionString);
    if (OutputString->LengthAllocated >= FileVersionLength) {
        memcpy(OutputString->StartOfString, Context->Entry.FileVersionString, FileVersionLength * sizeof(TCHAR));
        OutputString->LengthInChars = FileVersionLength;
    }
    return FileVersionLength;
}

/**
 Output the fragment count.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputFragmentCount(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    return FInfoOutputLargeInteger(Context->Entry.FragmentCount, 10, OutputString);
}

/**
 Output the link count.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputLinkCount(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    LARGE_INTEGER liTemp;
    liTemp.HighPart = 0;
    liTemp.LowPart = Context->Entry.LinkCount;
    return FInfoOutputLargeInteger(liTemp, 10, OutputString);
}

/**
 Output the file name.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputName(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    if (OutputString->LengthAllocated >= Context->Entry.FileNameLengthInChars) {
        memcpy(OutputString->StartOfString, Context->Entry.FileName, Context->Entry.FileNameLengthInChars * sizeof(TCHAR));
        OutputString->LengthInChars = Context->Entry.FileNameLengthInChars;
    }
    return Context->Entry.FileNameLengthInChars;
}

/**
 Output the major required OS version.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputOsVerMajor(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    LARGE_INTEGER liTemp;
    liTemp.HighPart = 0;
    liTemp.LowPart = Context->Entry.OsVersionHigh;
    return FInfoOutputLargeInteger(liTemp, 10, OutputString);
}

/**
 Output the minor required OS version.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputOsVerMinor(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    LARGE_INTEGER liTemp;
    liTemp.HighPart = 0;
    liTemp.LowPart = Context->Entry.OsVersionLow;
    return FInfoOutputLargeInteger(liTemp, 10, OutputString);
}

/**
 Output the owner.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputOwner(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    DWORD OwnerLength = _tcslen(Context->Entry.Owner);
    if (OutputString->LengthAllocated >= OwnerLength) {
        memcpy(OutputString->StartOfString, Context->Entry.Owner, OwnerLength * sizeof(TCHAR));
        OutputString->LengthInChars = OwnerLength;
    }
    return OwnerLength;
}

/**
 Output the reparse tag.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputReparseTag(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    LARGE_INTEGER liTemp;
    liTemp.HighPart = 0;
    liTemp.LowPart = Context->Entry.ReparseTag;
    return FInfoOutputLargeInteger(liTemp, 10, OutputString);
}

/**
 Output the reparse tag in hex.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputReparseTagHex(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    LARGE_INTEGER liTemp;
    liTemp.HighPart = 0;
    liTemp.LowPart = Context->Entry.ReparseTag;
    return FInfoOutputLargeInteger(liTemp, 16, OutputString);
}

/**
 Output the short file name.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputShortName(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    DWORD NameLength = _tcslen(Context->Entry.ShortFileName);
    if (OutputString->LengthAllocated >= NameLength) {
        memcpy(OutputString->StartOfString, Context->Entry.FileName, NameLength * sizeof(TCHAR));
        OutputString->LengthInChars = NameLength;
    }
    return NameLength;
}

/**
 Output the file size variable.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputSize(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    return FInfoOutputLargeInteger(Context->Entry.FileSize, 10, OutputString);
}

/**
 Output the file size variable in hex.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputSizeHex(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    return FInfoOutputLargeInteger(Context->Entry.FileSize, 16, OutputString);
}

/**
 Output the stream count.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputStreamCount(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    LARGE_INTEGER liTemp;
    liTemp.HighPart = 0;
    liTemp.LowPart = Context->Entry.StreamCount;
    return FInfoOutputLargeInteger(liTemp, 10, OutputString);
}

/**
 Output the USN.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputUsn(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    return FInfoOutputLargeInteger(Context->Entry.Usn, 10, OutputString);
}

/**
 Output the USN in hex.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputUsnHex(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    return FInfoOutputLargeInteger(Context->Entry.Usn, 16, OutputString);
}

/**
 Output the write date year.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputWriteDateYear(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    if (OutputString->LengthAllocated >= 4) {
        YoriLibYPrintf(OutputString, _T("%04i"), Context->Entry.WriteTime.wYear);
    }
    return 4;
}

/**
 Output the write date month.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputWriteDateMon(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    if (OutputString->LengthAllocated >= 2) {
        YoriLibYPrintf(OutputString, _T("%02i"), Context->Entry.WriteTime.wMonth);
    }
    return 2;
}

/**
 Output the write date day.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputWriteDateDay(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    if (OutputString->LengthAllocated >= 2) {
        YoriLibYPrintf(OutputString, _T("%02i"), Context->Entry.WriteTime.wDay);
    }
    return 2;
}

/**
 Output the write time hour.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputWriteTimeHour(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    if (OutputString->LengthAllocated >= 2) {
        YoriLibYPrintf(OutputString, _T("%02i"), Context->Entry.WriteTime.wHour);
    }
    return 2;
}

/**
 Output the write time minute.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputWriteTimeMin(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    if (OutputString->LengthAllocated >= 2) {
        YoriLibYPrintf(OutputString, _T("%02i"), Context->Entry.WriteTime.wMinute);
    }
    return 2;
}

/**
 Output the write time second.

 @param Context Pointer to context about the file, including previously
        obtained information to satisfy the output request.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
FInfoOutputWriteTimeSec(
    __in PFINFO_CONTEXT Context,
    __inout PYORI_STRING OutputString
    )
{
    if (OutputString->LengthAllocated >= 2) {
        YoriLibYPrintf(OutputString, _T("%02i"), Context->Entry.WriteTime.wSecond);
    }
    return 2;
}


/**
 Information about a single variable that this program can substitute with
 values about a file.
 */
typedef struct _FINFO_KNOWN_VARIABLE {

    /**
     A NULL terminated string corresponding to the name of the variable.
     */
    TCHAR VariableName[24];

    /**
     Pointer to a function which can obtain the variable contents from
     the system.
     */
    PFINFO_COLLECT_FN CollectFn;

    /**
     Pointer to a function which can convert the variable contents into
     an output string.
     */
    PFINFO_OUTPUT_FN OutputFn;

    /**
     Help text string.
     */
    TCHAR Help[64];
} FINFO_KNOWN_VARIABLE, *PFINFO_KNOWN_VARIABLE;

/**
 An array of known file variables and the functions needed to obtain and
 output information for those variables.
 */
FINFO_KNOWN_VARIABLE FInfoKnownVariables[] = {
    {_T("ACCESSDATE_YEAR"),    YoriLibCollectAccessTime,            FInfoOutputAccessDateYear,
     _T("The year when the file was last read.")},

    {_T("ACCESSDATE_MON"),     YoriLibCollectAccessTime,            FInfoOutputAccessDateMon,
     _T("The month when the file was last read.")},

    {_T("ACCESSDATE_DAY"),     YoriLibCollectAccessTime,            FInfoOutputAccessDateDay,
     _T("The day when the file was last read.")},

    {_T("ACCESSTIME_HOUR"),    YoriLibCollectAccessTime,            FInfoOutputAccessTimeHour,
     _T("The hour when the file was last read.")},

    {_T("ACCESSTIME_MIN"),     YoriLibCollectAccessTime,            FInfoOutputAccessTimeMin,
     _T("The minute when the file was last read.")},

    {_T("ACCESSTIME_SEC"),     YoriLibCollectAccessTime,            FInfoOutputAccessTimeSec,
     _T("The second when the file was last read.")},

    {_T("ALLOCRANGECOUNT"),    YoriLibCollectAllocatedRangeCount,   FInfoOutputAllocatedRangeCount,
     _T("The number of allocated ranges in the file.")},

    {_T("ALLOCSIZE"),          YoriLibCollectAllocationSize,        FInfoOutputAllocationSize,
     _T("The number of allocated bytes in the file in decimal.")},

    {_T("ALLOCSIZE_HEX"),      YoriLibCollectAllocationSize,        FInfoOutputAllocationSizeHex,
     _T("The number of allocated bytes in the file in hex.")},

    {_T("CASESENSITIVE"),      YoriLibCollectCaseSensitivity,       FInfoOutputCaseSensitivity,
     _T("The case sensitivity status of the directory.")},

    {_T("COMPRESSEDSIZE"),     YoriLibCollectCompressedFileSize,    FInfoOutputCompressedSize,
     _T("The number of compressed bytes in the file in decimal.")},

    {_T("COMPRESSEDSIZE_HEX"), YoriLibCollectCompressedFileSize,    FInfoOutputCompressedSizeHex,
     _T("The number of compressed bytes in the file in hex.")},

    {_T("CREATEDATE_YEAR"),    YoriLibCollectCreateTime,            FInfoOutputCreateDateYear,
     _T("The year when the file was created.")},

    {_T("CREATEDATE_MON"),     YoriLibCollectCreateTime,            FInfoOutputCreateDateMon,
     _T("The month when the file was created.")},

    {_T("CREATEDATE_DAY"),     YoriLibCollectCreateTime,            FInfoOutputCreateDateDay,
     _T("The day when the file was created.")},

    {_T("CREATETIME_HOUR"),    YoriLibCollectCreateTime,            FInfoOutputCreateTimeHour,
     _T("The hour when the file was created.")},

    {_T("CREATETIME_MIN"),     YoriLibCollectCreateTime,            FInfoOutputCreateTimeMin,
     _T("The minute when the file was created.")},

    {_T("CREATETIME_SEC"),     YoriLibCollectCreateTime,            FInfoOutputCreateTimeSec,
     _T("The second when the file was created.")},

    {_T("DESCRIPTION"),        YoriLibCollectDescription,           FInfoOutputDescription,
     _T("The executable's description.")},

    {_T("EXT"),                YoriLibCollectFileName,              FInfoOutputExt,
     _T("The file extension.")},

    {_T("FILEID"),             YoriLibCollectFileId,                FInfoOutputFileId,
     _T("The file ID in decimal.")},

    {_T("FILEID_HEX"),         YoriLibCollectFileId,                FInfoOutputFileIdHex,
     _T("The file ID in hex.")},

    {_T("FILEVERSTRING"),      YoriLibCollectFileVersionString,     FInfoOutputFileVersionString,
     _T("The file version string.")},

    {_T("FRAGMENTCOUNT"),      YoriLibCollectFragmentCount,         FInfoOutputFragmentCount,
     _T("The number of extents in the file.")},

    {_T("LINKCOUNT"),          YoriLibCollectLinkCount,             FInfoOutputLinkCount,
     _T("The number of hardlinks on the file.")},

    {_T("NAME"),               YoriLibCollectFileName,              FInfoOutputName,
     _T("The file name.")},

    {_T("OSVERMAJOR"),         YoriLibCollectOsVersion,             FInfoOutputOsVerMajor,
     _T("The minimum major OS version required by the program.")},

    {_T("OSVERMINOR"),         YoriLibCollectOsVersion,             FInfoOutputOsVerMinor,
     _T("The minimum minor OS version required by the program.")},

    {_T("OWNER"),              YoriLibCollectOwner,                 FInfoOutputOwner,
     _T("The owner of the file.")},

    {_T("REPARSETAG"),         YoriLibCollectReparseTag,            FInfoOutputReparseTag,
     _T("The reparse tag in decimal.")},

    {_T("REPARSETAG_HEX"),     YoriLibCollectReparseTag,            FInfoOutputReparseTagHex,
     _T("The reparse tag in hex.")},

    {_T("SIZE"),               YoriLibCollectFileSize,              FInfoOutputSize,
     _T("The file size in bytes.")},

    {_T("SHORTNAME"),          YoriLibCollectShortName,             FInfoOutputShortName,
     _T("The short file name.")},

    {_T("STREAMCOUNT"),        YoriLibCollectStreamCount,           FInfoOutputStreamCount,
     _T("The number of named streams on the file.")},

    {_T("USN"),                YoriLibCollectUsn,                   FInfoOutputUsn,
     _T("The USN on the file in decimal.")},

    {_T("USN_HEX"),            YoriLibCollectUsn,                   FInfoOutputUsnHex,
     _T("The USN on the file in hex.")},

    {_T("WRITEDATE_YEAR"),     YoriLibCollectWriteTime,             FInfoOutputWriteDateYear,
     _T("The year when the file was last written to.")},

    {_T("WRITEDATE_MON"),      YoriLibCollectWriteTime,             FInfoOutputWriteDateMon,
     _T("The month when the file was last written to.")},

    {_T("WRITEDATE_DAY"),      YoriLibCollectWriteTime,             FInfoOutputWriteDateDay,
     _T("The day when the file was last written to.")},

    {_T("WRITETIME_HOUR"),     YoriLibCollectWriteTime,             FInfoOutputWriteTimeHour,
     _T("The hour when the file was last written to.")},

    {_T("WRITETIME_MIN"),      YoriLibCollectWriteTime,             FInfoOutputWriteTimeMin,
     _T("The minute when the file was last written to.")},

    {_T("WRITETIME_SEC"),      YoriLibCollectWriteTime,             FInfoOutputWriteTimeSec,
     _T("The second when the file was last written to.")}
};

/**
 Display usage text to the user.
 */
BOOL
FInfoHelp(VOID)
{
    DWORD Count;
    TCHAR NameWithQualifiers[32];

    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("FInfo %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs\n"), strFInfoHelpText);
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Format specifiers are:\n\n"));
    for (Count = 0; Count < sizeof(FInfoKnownVariables)/sizeof(FInfoKnownVariables[0]); Count++) {
        YoriLibSPrintfS(NameWithQualifiers, sizeof(NameWithQualifiers)/sizeof(NameWithQualifiers[0]), _T("$%s$"), FInfoKnownVariables[Count].VariableName);
        YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%-20s %s\n"), NameWithQualifiers, FInfoKnownVariables[Count].Help);
    }
    return TRUE;
}

/**
 Expand any variables in the format string of information to display for each
 file.

 @param OutputString The buffer to populate with the result of variable
        expansion.

 @param VariableName The name of the variable to populate data from.

 @param Context Pointer to a FINFO_CONTEXT structure containing state about
        the file.

 @return The number of characters populated or number of characters required
         to successfully populate the variable contents.
 */
DWORD
FInfoExpandVariables(
    __inout PYORI_STRING OutputString,
    __in PYORI_STRING VariableName,
    __in PVOID Context
    )
{
    DWORD Index;
    DWORD CharsNeeded = 0;
    PFINFO_CONTEXT FInfoContext = (PFINFO_CONTEXT)Context;

    for (Index = 0; Index < sizeof(FInfoKnownVariables)/sizeof(FInfoKnownVariables[0]); Index++) {
        if (YoriLibCompareStringWithLiteral(VariableName, FInfoKnownVariables[Index].VariableName) == 0) {
            FInfoKnownVariables[Index].CollectFn(&FInfoContext->Entry, FInfoContext->FileInfo, FInfoContext->FilePath);
            CharsNeeded = FInfoKnownVariables[Index].OutputFn(FInfoContext, OutputString);
        }
    }

    return CharsNeeded;
}

/**
 A callback that is invoked when a file is found that matches a search criteria
 specified in the set of strings to enumerate.

 @param FilePath Pointer to the file path that was found.

 @param FileInfo Information about the file.

 @param Depth Specifies recursion depth.  Ignored in this application.

 @param Context Pointer to the finfo context structure indicating the
        action to perform and populated with the file and line count found.

 @return TRUE to continute enumerating, FALSE to abort.
 */
BOOL
FInfoFileFoundCallback(
    __in PYORI_STRING FilePath,
    __in_opt PWIN32_FIND_DATA FileInfo,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    YORI_STRING DisplayString;
    WIN32_FIND_DATA LocalFileInfo;
    PWIN32_FIND_DATA FileInfoToUse;
    PFINFO_CONTEXT FInfoContext;

    UNREFERENCED_PARAMETER(Depth);
    ASSERT(YoriLibIsStringNullTerminated(FilePath));

    FInfoContext = (PFINFO_CONTEXT)Context;

    if (FileInfo != NULL) {
        FileInfoToUse = FileInfo;
    } else {
        ZeroMemory(&LocalFileInfo, sizeof(LocalFileInfo));
        YoriLibUpdateFindDataFromFileInformation(&LocalFileInfo, FilePath->StartOfString, TRUE);
        FileInfoToUse = &LocalFileInfo;
    }

    FInfoContext->FilePath = FilePath;
    FInfoContext->FileInfo = FileInfoToUse;
    FInfoContext->FilesFound++;
    FInfoContext->FilesFoundThisArg++;

    YoriLibInitEmptyString(&DisplayString);
    YoriLibExpandCommandVariables(&FInfoContext->FormatString, '$', TRUE, FInfoExpandVariables, FInfoContext, &DisplayString);
    if (DisplayString.StartOfString != NULL) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%y"), &DisplayString);
        YoriLibFreeStringContents(&DisplayString);
    }

    return TRUE;
}

/**
 A default format string to use if the user didn't specify one.
 */
TCHAR DefaultFormatString[] = 
    _T("NAME:             $NAME$\n")
    _T("ACCESSDATE:       $ACCESSDATE_YEAR$/$ACCESSDATE_MON$/$ACCESSDATE_DAY$\n")
    _T("ACCESSTIME:       $ACCESSTIME_HOUR$:$ACCESSTIME_MIN$:$ACCESSTIME_SEC$\n")
    _T("CREATEDATE:       $CREATEDATE_YEAR$/$CREATEDATE_MON$/$CREATEDATE_DAY$\n")
    _T("CREATETIME:       $CREATETIME_HOUR$:$CREATETIME_MIN$:$CREATETIME_SEC$\n")
    _T("ALLOCRANGECOUNT:  $ALLOCRANGECOUNT$\n")
    _T("ALLOCSIZE:        $ALLOCSIZE$\n")
    _T("CASESENSITIVE:    $CASESENSITIVE$\n")
    _T("COMPRESSEDSIZE:   $COMPRESSEDSIZE$\n")
    _T("EXT:              $EXT$\n")
    _T("FILEID_HEX:       $FILEID_HEX$\n")
    _T("FRAGMENTCOUNT:    $FRAGMENTCOUNT$\n")
    _T("LINKCOUNT:        $LINKCOUNT$\n")
    _T("OSVERMAJOR:       $OSVERMAJOR$\n")
    _T("OSVERMINOR:       $OSVERMINOR$\n")
    _T("OWNER:            $OWNER$\n")
    _T("REPARSETAG_HEX:   $REPARSETAG_HEX$\n")
    _T("SIZE:             $SIZE$\n")
    _T("SHORTNAME:        $SHORTNAME$\n")
    _T("STREAMCOUNT:      $STREAMCOUNT$\n")
    _T("WRITEDATE:        $WRITEDATE_YEAR$/$WRITEDATE_MON$/$WRITEDATE_DAY$\n")
    _T("WRITETIME:        $WRITETIME_HOUR$:$WRITETIME_MIN$:$WRITETIME_SEC$\n");

#ifdef YORI_BUILTIN
/**
 The main entrypoint for the finfo builtin command.
 */
#define ENTRYPOINT YoriCmd_FINFO
#else
/**
 The main entrypoint for the finfo standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the finfo cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process, zero on success, nonzero on failure.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    DWORD MatchFlags;
    BOOL Recursive = FALSE;
    BOOL BasicEnumeration = FALSE;
    BOOL ReturnDirectories = FALSE;
    FINFO_CONTEXT FInfoContext;
    YORI_STRING Arg;

    ZeroMemory(&FInfoContext, sizeof(FInfoContext));
    YoriLibConstantString(&FInfoContext.FormatString, DefaultFormatString);

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                FInfoHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2018-2021"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("b")) == 0) {
                BasicEnumeration = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("d")) == 0) {
                ReturnDirectories = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("f")) == 0) {
                if (i + 1 < ArgC) {
                    ArgumentUnderstood = TRUE;
                    i++;
                    memcpy(&FInfoContext.FormatString, &ArgV[i], sizeof(YORI_STRING));
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("s")) == 0) {
                Recursive = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                ArgumentUnderstood = TRUE;
                StartArg = i + 1;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

#if YORI_BUILTIN
    YoriLibCancelEnable();
#endif

    //
    //  If no file name is specified, use stdin; otherwise open
    //  the file and use that
    //

    if (StartArg == 0 || StartArg == ArgC) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("finfo: missing argument\n"));
        return EXIT_FAILURE;
    } else {
        MatchFlags = YORILIB_FILEENUM_RETURN_FILES;

        if (ReturnDirectories) {
            MatchFlags |= YORILIB_FILEENUM_RETURN_DIRECTORIES;
        } else {
            MatchFlags |= YORILIB_FILEENUM_DIRECTORY_CONTENTS;
        }

        if (Recursive) {
            MatchFlags |= YORILIB_FILEENUM_RECURSE_BEFORE_RETURN | YORILIB_FILEENUM_RECURSE_PRESERVE_WILD;
        }

        if (BasicEnumeration) {
            MatchFlags |= YORILIB_FILEENUM_BASIC_EXPANSION;
        }

        for (i = StartArg; i < ArgC; i++) {

            FInfoContext.FilesFoundThisArg = 0;
            YoriLibForEachFile(&ArgV[i], MatchFlags, 0, FInfoFileFoundCallback, NULL, &FInfoContext);
            if (FInfoContext.FilesFoundThisArg == 0) {
                YORI_STRING FullPath;
                YoriLibInitEmptyString(&FullPath);
                if (YoriLibUserStringToSingleFilePath(&ArgV[i], TRUE, &FullPath)) {
                    FInfoFileFoundCallback(&FullPath, NULL, 0, &FInfoContext);
                    YoriLibFreeStringContents(&FullPath);
                }
            }
        }
    }

    if (FInfoContext.FilesFound == 0) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("finfo: no matching files found\n"));
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>lib/dyld.c<|end_filename|>
/**
 * @file lib/dyld.c
 *
 * Yori dynamically loaded OS function support
 *
 * Copyright (c) 2018-2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "yoripch.h"
#include "yorilib.h"

/**
 Map a function name to a location in memory to store a function pointer.
 */
typedef struct _YORI_DLL_NAME_MAP {

    /**
     Pointer to a memory location to be updated with a function pointer when
     the specified function is resolved.
     */
    FARPROC * FnPtr;

    /**
     The name of the function to resolve.
     */
    LPCSTR FnName;
} YORI_DLL_NAME_MAP, *PYORI_DLL_NAME_MAP;

/**
 Load a DLL from the System32 directory.

 @param DllName Pointer to the name of the DLL to load.

 @return Module to the DLL, or NULL on failure.
 */
HMODULE
YoriLibLoadLibraryFromSystemDirectory(
    __in LPCTSTR DllName
    )
{
    DWORD LengthRequired;
    YORI_STRING YsDllName;
    YORI_STRING FullPath;
    HMODULE DllModule;

    YoriLibConstantString(&YsDllName, DllName);
    LengthRequired = GetSystemDirectory(NULL, 0);

    if (!YoriLibAllocateString(&FullPath, LengthRequired + 1 + YsDllName.LengthInChars + 1)) {
        return NULL;
    }

    FullPath.LengthInChars = GetSystemDirectory(FullPath.StartOfString, LengthRequired);
    if (FullPath.LengthInChars == 0) {
        YoriLibFreeStringContents(&FullPath);
        return NULL;
    }

    FullPath.LengthInChars += YoriLibSPrintf(&FullPath.StartOfString[FullPath.LengthInChars], _T("\\%y"), &YsDllName);

    DllModule = LoadLibrary(FullPath.StartOfString);

    YoriLibFreeStringContents(&FullPath);
    return DllModule;
}

/**
 A structure containing pointers to ntdll.dll functions that can be used if
 they are found but programs do not have a hard dependency on.
 */
YORI_NTDLL_FUNCTIONS DllNtDll;

/**
 Load pointers to all optional ntdll.dll functions.  Because ntdll.dll is
 effectively mandatory in any Win32 process, this uses GetModuleHandle rather
 than LoadLibrary and pointers are valid for the lifetime of the process.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriLibLoadNtDllFunctions(VOID)
{
    if (DllNtDll.hDll != NULL) {
        return TRUE;
    }

    DllNtDll.hDll = GetModuleHandle(_T("NTDLL"));
    if (DllNtDll.hDll == NULL) {
        return FALSE;
    }
    DllNtDll.pNtQueryInformationFile = (PNT_QUERY_INFORMATION_FILE)GetProcAddress(DllNtDll.hDll, "NtQueryInformationFile");
    DllNtDll.pNtQueryInformationProcess = (PNT_QUERY_INFORMATION_PROCESS)GetProcAddress(DllNtDll.hDll, "NtQueryInformationProcess");
    DllNtDll.pNtQueryInformationThread = (PNT_QUERY_INFORMATION_THREAD)GetProcAddress(DllNtDll.hDll, "NtQueryInformationThread");
    DllNtDll.pNtQueryObject = (PNT_QUERY_OBJECT)GetProcAddress(DllNtDll.hDll, "NtQueryObject");
    DllNtDll.pNtQuerySystemInformation = (PNT_QUERY_SYSTEM_INFORMATION)GetProcAddress(DllNtDll.hDll, "NtQuerySystemInformation");
    DllNtDll.pNtSetInformationFile = (PNT_SET_INFORMATION_FILE)GetProcAddress(DllNtDll.hDll, "NtSetInformationFile");
    DllNtDll.pNtSystemDebugControl = (PNT_SYSTEM_DEBUG_CONTROL)GetProcAddress(DllNtDll.hDll, "NtSystemDebugControl");
    DllNtDll.pRtlGetLastNtStatus = (PRTL_GET_LAST_NT_STATUS)GetProcAddress(DllNtDll.hDll, "RtlGetLastNtStatus");
    return TRUE;
}

/**
 A structure containing pointers to kernel32.dll functions that can be used if
 they are found but programs do not have a hard dependency on.
 */
YORI_KERNEL32_FUNCTIONS DllKernel32;

/**
 The set of functions to resolve from kernel32.dll.
 */
CONST YORI_DLL_NAME_MAP DllKernel32Symbols[] = {
    {(FARPROC *)&DllKernel32.pAddConsoleAliasW, "AddConsoleAliasW"},
    {(FARPROC *)&DllKernel32.pAssignProcessToJobObject, "AssignProcessToJobObject"},
    {(FARPROC *)&DllKernel32.pCreateHardLinkW, "CreateHardLinkW"},
    {(FARPROC *)&DllKernel32.pCreateJobObjectW, "CreateJobObjectW"},
    {(FARPROC *)&DllKernel32.pCreateSymbolicLinkW, "CreateSymbolicLinkW"},
    {(FARPROC *)&DllKernel32.pFindFirstStreamW, "FindFirstStreamW"},
    {(FARPROC *)&DllKernel32.pFindFirstVolumeW, "FindFirstVolumeW"},
    {(FARPROC *)&DllKernel32.pFindNextStreamW, "FindNextStreamW"},
    {(FARPROC *)&DllKernel32.pFindNextVolumeW, "FindNextVolumeW"},
    {(FARPROC *)&DllKernel32.pFindVolumeClose, "FindVolumeClose"},
    {(FARPROC *)&DllKernel32.pFreeEnvironmentStringsW, "FreeEnvironmentStringsW"},
    {(FARPROC *)&DllKernel32.pGetCompressedFileSizeW, "GetCompressedFileSizeW"},
    {(FARPROC *)&DllKernel32.pGetConsoleAliasesLengthW, "GetConsoleAliasesLengthW"},
    {(FARPROC *)&DllKernel32.pGetConsoleAliasesW, "GetConsoleAliasesW"},
    {(FARPROC *)&DllKernel32.pGetConsoleScreenBufferInfoEx, "GetConsoleScreenBufferInfoEx"},
    {(FARPROC *)&DllKernel32.pGetConsoleProcessList, "GetConsoleProcessList"},
    {(FARPROC *)&DllKernel32.pGetConsoleWindow, "GetConsoleWindow"},
    {(FARPROC *)&DllKernel32.pGetCurrentConsoleFontEx, "GetCurrentConsoleFontEx"},
    {(FARPROC *)&DllKernel32.pGetDiskFreeSpaceExW, "GetDiskFreeSpaceExW"},
    {(FARPROC *)&DllKernel32.pGetEnvironmentStrings, "GetEnvironmentStrings"},
    {(FARPROC *)&DllKernel32.pGetEnvironmentStringsW, "GetEnvironmentStringsW"},
    {(FARPROC *)&DllKernel32.pGetFileInformationByHandleEx, "GetFileInformationByHandleEx"},
    {(FARPROC *)&DllKernel32.pGetFinalPathNameByHandleW, "GetFinalPathNameByHandleW"},
    {(FARPROC *)&DllKernel32.pGetLogicalProcessorInformation, "GetLogicalProcessorInformation"},
    {(FARPROC *)&DllKernel32.pGetLogicalProcessorInformationEx, "GetLogicalProcessorInformationEx"},
    {(FARPROC *)&DllKernel32.pGetNativeSystemInfo, "GetNativeSystemInfo"},
    {(FARPROC *)&DllKernel32.pGetPrivateProfileSectionNamesW, "GetPrivateProfileSectionNamesW"},
    {(FARPROC *)&DllKernel32.pGetProcessIoCounters, "GetProcessIoCounters"},
    {(FARPROC *)&DllKernel32.pGetProductInfo, "GetProductInfo"},
    {(FARPROC *)&DllKernel32.pGetTickCount64, "GetTickCount64"},
    {(FARPROC *)&DllKernel32.pGetVersionExW, "GetVersionExW"},
    {(FARPROC *)&DllKernel32.pGetVolumePathNamesForVolumeNameW, "GetVolumePathNamesForVolumeNameW"},
    {(FARPROC *)&DllKernel32.pGetVolumePathNameW, "GetVolumePathNameW"},
    {(FARPROC *)&DllKernel32.pGlobalMemoryStatusEx, "GlobalMemoryStatusEx"},
    {(FARPROC *)&DllKernel32.pInterlockedCompareExchange, "InterlockedCompareExchange"},
    {(FARPROC *)&DllKernel32.pIsWow64Process, "IsWow64Process"},
    {(FARPROC *)&DllKernel32.pIsWow64Process2, "IsWow64Process2"},
    {(FARPROC *)&DllKernel32.pOpenThread, "OpenThread"},
    {(FARPROC *)&DllKernel32.pQueryFullProcessImageNameW, "QueryFullProcessImageNameW"},
    {(FARPROC *)&DllKernel32.pQueryInformationJobObject, "QueryInformationJobObject"},
    {(FARPROC *)&DllKernel32.pRegisterApplicationRestart, "RegisterApplicationRestart"},
    {(FARPROC *)&DllKernel32.pReplaceFileW, "ReplaceFileW"},
    {(FARPROC *)&DllKernel32.pRtlCaptureStackBackTrace, "RtlCaptureStackBackTrace"},
    {(FARPROC *)&DllKernel32.pSetConsoleScreenBufferInfoEx, "SetConsoleScreenBufferInfoEx"},
    {(FARPROC *)&DllKernel32.pSetCurrentConsoleFontEx, "SetCurrentConsoleFontEx"},
    {(FARPROC *)&DllKernel32.pSetFileInformationByHandle, "SetFileInformationByHandle"},
    {(FARPROC *)&DllKernel32.pSetInformationJobObject, "SetInformationJobObject"},
    {(FARPROC *)&DllKernel32.pWow64DisableWow64FsRedirection, "Wow64DisableWow64FsRedirection"},
    {(FARPROC *)&DllKernel32.pWow64GetThreadContext, "Wow64GetThreadContext"},
    {(FARPROC *)&DllKernel32.pWow64SetThreadContext, "Wow64SetThreadContext"},
};


/**
 Load pointers to all optional kernel32.dll functions.  Because kernel32.dll is
 effectively mandatory in any Win32 process, this uses GetModuleHandle rather
 than LoadLibrary and pointers are valid for the lifetime of the process.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriLibLoadKernel32Functions(VOID)
{
    DWORD Count;
    if (DllKernel32.hDll != NULL) {
        return TRUE;
    }

    DllKernel32.hDll = GetModuleHandle(_T("KERNEL32"));
    if (DllKernel32.hDll == NULL) {
        return FALSE;
    }

    for (Count = 0; Count < sizeof(DllKernel32Symbols)/sizeof(DllKernel32Symbols[0]); Count++) {
        *(DllKernel32Symbols[Count].FnPtr) = GetProcAddress(DllKernel32.hDll, DllKernel32Symbols[Count].FnName);
    }

    return TRUE;
}

/**
 A structure containing pointers to ctl3d32.dll functions that can be used if
 they are found but programs do not have a hard dependency on.
 */
YORI_CTL3D_FUNCTIONS DllCtl3d;

/**
 Load pointers to all optional Ctl3d32.dll functions.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriLibLoadCtl3d32Functions(VOID)
{

    if (DllCtl3d.hDll != NULL) {
        return TRUE;
    }

    DllCtl3d.hDll = YoriLibLoadLibraryFromSystemDirectory(_T("CTL3D32.DLL"));
    if (DllCtl3d.hDll == NULL) {
        return FALSE;
    }

    DllCtl3d.pCtl3dAutoSubclass = (PCTL3D_AUTOSUBCLASS)GetProcAddress(DllCtl3d.hDll, "Ctl3dAutoSubclass");
    DllCtl3d.pCtl3dRegister = (PCTL3D_REGISTER)GetProcAddress(DllCtl3d.hDll, "Ctl3dRegister");

    return TRUE;
}

/**
 A structure containing pointers to dbghelp.dll functions that can be used if
 they are found but programs do not have a hard dependency on.
 */
YORI_DBGHELP_FUNCTIONS DllDbgHelp;

/**
 Load pointers to all optional dbghelp.dll functions.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriLibLoadDbgHelpFunctions(VOID)
{

    if (DllDbgHelp.hDll != NULL) {
        return TRUE;
    }

    DllDbgHelp.hDll = YoriLibLoadLibraryFromSystemDirectory(_T("DBGHELP.DLL"));
    if (DllDbgHelp.hDll == NULL) {
        return FALSE;
    }

    DllDbgHelp.pMiniDumpWriteDump = (PMINI_DUMP_WRITE_DUMP)GetProcAddress(DllDbgHelp.hDll, "MiniDumpWriteDump");

    return TRUE;
}

/**
 A structure containing pointers to imagehlp.dll functions that can be used if
 they are found but programs do not have a hard dependency on.
 */
YORI_IMAGEHLP_FUNCTIONS DllImageHlp;

/**
 Load pointers to all optional imagehlp.dll functions.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriLibLoadImageHlpFunctions(VOID)
{

    if (DllImageHlp.hDll != NULL) {
        return TRUE;
    }

    DllImageHlp.hDll = YoriLibLoadLibraryFromSystemDirectory(_T("IMAGEHLP.DLL"));
    if (DllImageHlp.hDll == NULL) {
        return FALSE;
    }

    DllImageHlp.pMapFileAndCheckSumW = (PMAP_FILE_AND_CHECKSUMW)GetProcAddress(DllImageHlp.hDll, "MapFileAndCheckSumW");

    return TRUE;
}

/**
 A structure containing pointers to ole32.dll functions that can be used if
 they are found but programs do not have a hard dependency on.
 */
YORI_OLE32_FUNCTIONS DllOle32;

/**
 Load pointers to all optional ole32.dll functions.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriLibLoadOle32Functions(VOID)
{

    if (DllOle32.hDll != NULL) {
        return TRUE;
    }

    DllOle32.hDll = YoriLibLoadLibraryFromSystemDirectory(_T("OLE32.DLL"));
    if (DllOle32.hDll == NULL) {
        return FALSE;
    }

    DllOle32.pCoCreateInstance = (PCO_CREATE_INSTANCE)GetProcAddress(DllOle32.hDll, "CoCreateInstance");
    DllOle32.pCoInitialize = (PCO_INITIALIZE)GetProcAddress(DllOle32.hDll, "CoInitialize");
    DllOle32.pCoTaskMemFree = (PCO_TASK_MEM_FREE)GetProcAddress(DllOle32.hDll, "CoTaskMemFree");

    return TRUE;
}

/**
 A structure containing pointers to psapi.dll functions that can be used if
 they are found but programs do not have a hard dependency on.
 */
YORI_PSAPI_FUNCTIONS DllPsapi;

/**
 Load pointers to all optional psapi.dll functions.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriLibLoadPsapiFunctions(VOID)
{

    if (DllPsapi.hDll != NULL) {
        return TRUE;
    }

    DllPsapi.hDll = YoriLibLoadLibraryFromSystemDirectory(_T("PSAPI.DLL"));
    if (DllPsapi.hDll == NULL) {
        return FALSE;
    }

    DllPsapi.pGetModuleFileNameExW = (PGET_MODULE_FILE_NAME_EXW)GetProcAddress(DllPsapi.hDll, "GetModuleFileNameExW");

    return TRUE;
}

/**
 GUID to fetch the downloads folder on Vista+.
 */
const GUID FOLDERID_Downloads = { 0x374de290, 0x123f, 0x4565, { 0x91, 0x64, 0x39, 0xc4, 0x92, 0x5e, 0x46, 0x7b } };

/**
 A structure containing pointers to shell32.dll functions that can be used if
 they are found but programs do not have a hard dependency on.
 */
YORI_SHELL32_FUNCTIONS DllShell32;

/**
 Load pointers to all optional shell32.dll functions.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriLibLoadShell32Functions(VOID)
{

    if (DllShell32.hDll != NULL) {
        return TRUE;
    }

    DllShell32.hDll = YoriLibLoadLibraryFromSystemDirectory(_T("SHELL32.DLL"));
    if (DllShell32.hDll == NULL) {
        return FALSE;
    }

    DllShell32.pSHAppBarMessage = (PSH_APP_BAR_MESSAGE)GetProcAddress(DllShell32.hDll, "SHAppBarMessage");
    DllShell32.pSHBrowseForFolderW = (PSH_BROWSE_FOR_FOLDERW)GetProcAddress(DllShell32.hDll, "SHBrowseForFolderW");
    DllShell32.pSHFileOperationW = (PSH_FILE_OPERATIONW)GetProcAddress(DllShell32.hDll, "SHFileOperationW");
    DllShell32.pSHGetKnownFolderPath = (PSH_GET_KNOWN_FOLDER_PATH)GetProcAddress(DllShell32.hDll, "SHGetKnownFolderPath");
    DllShell32.pSHGetPathFromIDListW = (PSH_GET_PATH_FROM_ID_LISTW)GetProcAddress(DllShell32.hDll, "SHGetPathFromIDListW");
    DllShell32.pSHGetSpecialFolderPathW = (PSH_GET_SPECIAL_FOLDER_PATHW)GetProcAddress(DllShell32.hDll, "SHGetSpecialFolderPathW");
    DllShell32.pShellExecuteExW = (PSHELL_EXECUTE_EXW)GetProcAddress(DllShell32.hDll, "ShellExecuteExW");
    DllShell32.pShellExecuteW = (PSHELL_EXECUTEW)GetProcAddress(DllShell32.hDll, "ShellExecuteW");
    return TRUE;
}

/**
 A structure containing pointers to shfolder.dll functions that can be used if
 they are found but programs do not have a hard dependency on.
 */
YORI_SHFOLDER_FUNCTIONS DllShfolder;

/**
 Load pointers to all optional shfolder.dll functions.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriLibLoadShfolderFunctions(VOID)
{

    if (DllShfolder.hDll != NULL) {
        return TRUE;
    }

    DllShfolder.hDll = YoriLibLoadLibraryFromSystemDirectory(_T("SHFOLDER.DLL"));
    if (DllShfolder.hDll == NULL) {
        return FALSE;
    }

    DllShfolder.pSHGetFolderPathW = (PSH_GET_FOLDER_PATHW)GetProcAddress(DllShfolder.hDll, "SHGetFolderPathW");
    return TRUE;
}

/**
 A structure containing pointers to user32.dll functions that can be used if
 they are found but programs do not have a hard dependency on.
 */
YORI_USER32_FUNCTIONS DllUser32;

/**
 Load pointers to all optional user32.dll functions.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriLibLoadUser32Functions(VOID)
{
    if (DllUser32.hDll != NULL) {
        return TRUE;
    }

    DllUser32.hDll = YoriLibLoadLibraryFromSystemDirectory(_T("USER32.DLL"));
    if (DllUser32.hDll == NULL) {
        return FALSE;
    }

    DllUser32.pCascadeWindows = (PCASCADE_WINDOWS)GetProcAddress(DllUser32.hDll, "CascadeWindows");
    DllUser32.pCloseClipboard = (PCLOSE_CLIPBOARD)GetProcAddress(DllUser32.hDll, "CloseClipboard");
    DllUser32.pDdeClientTransaction = (PDDE_CLIENT_TRANSACTION)GetProcAddress(DllUser32.hDll, "DdeClientTransaction");
    DllUser32.pDdeConnect = (PDDE_CONNECT)GetProcAddress(DllUser32.hDll, "DdeConnect");
    DllUser32.pDdeCreateDataHandle = (PDDE_CREATE_DATA_HANDLE)GetProcAddress(DllUser32.hDll, "DdeCreateDataHandle");
    DllUser32.pDdeCreateStringHandleW = (PDDE_CREATE_STRING_HANDLEW)GetProcAddress(DllUser32.hDll, "DdeCreateStringHandleW");
    DllUser32.pDdeDisconnect = (PDDE_DISCONNECT)GetProcAddress(DllUser32.hDll, "DdeDisconnect");
    DllUser32.pDdeFreeStringHandle = (PDDE_FREE_STRING_HANDLE)GetProcAddress(DllUser32.hDll, "DdeFreeStringHandle");
    DllUser32.pDdeInitializeW = (PDDE_INITIALIZEW)GetProcAddress(DllUser32.hDll, "DdeInitializeW");
    DllUser32.pDdeUninitialize = (PDDE_UNINITIALIZE)GetProcAddress(DllUser32.hDll, "DdeUninitialize");
    DllUser32.pEmptyClipboard = (PEMPTY_CLIPBOARD)GetProcAddress(DllUser32.hDll, "EmptyClipboard");
    DllUser32.pEnumClipboardFormats = (PENUM_CLIPBOARD_FORMATS)GetProcAddress(DllUser32.hDll, "EnumClipboardFormats");
    DllUser32.pExitWindowsEx = (PEXIT_WINDOWS_EX)GetProcAddress(DllUser32.hDll, "ExitWindowsEx");
    DllUser32.pFindWindowW = (PFIND_WINDOWW)GetProcAddress(DllUser32.hDll, "FindWindowW");
    DllUser32.pGetClipboardData = (PGET_CLIPBOARD_DATA)GetProcAddress(DllUser32.hDll, "GetClipboardData");
    DllUser32.pGetClipboardFormatNameW = (PGET_CLIPBOARD_FORMAT_NAMEW)GetProcAddress(DllUser32.hDll, "GetClipboardFormatNameW");
    DllUser32.pGetClientRect = (PGET_CLIENT_RECT)GetProcAddress(DllUser32.hDll, "GetClientRect");
    DllUser32.pGetDesktopWindow = (PGET_DESKTOP_WINDOW)GetProcAddress(DllUser32.hDll, "GetDesktopWindow");
    DllUser32.pGetKeyboardLayout = (PGET_KEYBOARD_LAYOUT)GetProcAddress(DllUser32.hDll, "GetKeyboardLayout");
    DllUser32.pGetWindowRect = (PGET_WINDOW_RECT)GetProcAddress(DllUser32.hDll, "GetWindowRect");
    DllUser32.pLockWorkStation = (PLOCK_WORKSTATION)GetProcAddress(DllUser32.hDll, "LockWorkStation");
    DllUser32.pMoveWindow = (PMOVE_WINDOW)GetProcAddress(DllUser32.hDll, "MoveWindow");
    DllUser32.pOpenClipboard = (POPEN_CLIPBOARD)GetProcAddress(DllUser32.hDll, "OpenClipboard");
    DllUser32.pRegisterClipboardFormatW = (PREGISTER_CLIPBOARD_FORMATW)GetProcAddress(DllUser32.hDll, "RegisterClipboardFormatW");
    DllUser32.pRegisterShellHookWindow = (PREGISTER_SHELL_HOOK_WINDOW)GetProcAddress(DllUser32.hDll, "RegisterShellHookWindow");
    DllUser32.pSendMessageTimeoutW = (PSEND_MESSAGE_TIMEOUTW)GetProcAddress(DllUser32.hDll, "SendMessageTimeoutW");
    DllUser32.pSetClipboardData = (PSET_CLIPBOARD_DATA)GetProcAddress(DllUser32.hDll, "SetClipboardData");
    DllUser32.pSetForegroundWindow = (PSET_FOREGROUND_WINDOW)GetProcAddress(DllUser32.hDll, "SetForegroundWindow");
    DllUser32.pSetWindowTextW = (PSET_WINDOW_TEXTW)GetProcAddress(DllUser32.hDll, "SetWindowTextW");
    DllUser32.pShowWindow = (PSHOW_WINDOW)GetProcAddress(DllUser32.hDll, "ShowWindow");
    DllUser32.pTileWindows = (PTILE_WINDOWS)GetProcAddress(DllUser32.hDll, "TileWindows");

    return TRUE;
}

/**
 A structure containing pointers to version.dll functions that can be used if
 they are found but programs do not have a hard dependency on.
 */
YORI_VERSION_FUNCTIONS DllVersion;

/**
 Load pointers to all optional version.dll functions.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriLibLoadVersionFunctions(VOID)
{

    if (DllVersion.hDll != NULL) {
        return TRUE;
    }

    DllVersion.hDll = YoriLibLoadLibraryFromSystemDirectory(_T("VERSION.DLL"));
    if (DllVersion.hDll == NULL) {
        return FALSE;
    }

    DllVersion.pGetFileVersionInfoSizeW = (PGET_FILE_VERSION_INFO_SIZEW)GetProcAddress(DllVersion.hDll, "GetFileVersionInfoSizeW");
    DllVersion.pGetFileVersionInfoW = (PGET_FILE_VERSION_INFOW)GetProcAddress(DllVersion.hDll, "GetFileVersionInfoW");
    DllVersion.pVerQueryValueW = (PVER_QUERY_VALUEW)GetProcAddress(DllVersion.hDll, "VerQueryValueW");

    return TRUE;
}

/**
 GUID for an unknown virtual storage implementations.
 */
const GUID VIRTUAL_STORAGE_TYPE_VENDOR_UNKNOWN = { 0x00000000, 0x0000, 0x0000, { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } };

/**
 GUID for Microsoft provided virtual storage implementations.
 */
const GUID VIRTUAL_STORAGE_TYPE_VENDOR_MICROSOFT = { 0xec984aec, 0xa0f9, 0x47e9, { 0x90, 0x1f, 0x71, 0x41, 0x5a, 0x66, 0x34, 0x5b } };

/**
 A structure containing pointers to virtdisk.dll functions that can be used if
 they are found but programs do not have a hard dependency on.
 */
YORI_VIRTDISK_FUNCTIONS DllVirtDisk;

/**
 Load pointers to all optional virtdisk.dll functions.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriLibLoadVirtDiskFunctions(VOID)
{

    if (DllVirtDisk.hDll != NULL) {
        return TRUE;
    }

    DllVirtDisk.hDll = YoriLibLoadLibraryFromSystemDirectory(_T("VIRTDISK.DLL"));
    if (DllVirtDisk.hDll == NULL) {
        return FALSE;
    }

    DllVirtDisk.pAttachVirtualDisk = (PATTACH_VIRTUAL_DISK)GetProcAddress(DllVirtDisk.hDll, "AttachVirtualDisk");
    DllVirtDisk.pCompactVirtualDisk = (PCOMPACT_VIRTUAL_DISK)GetProcAddress(DllVirtDisk.hDll, "CompactVirtualDisk");
    DllVirtDisk.pCreateVirtualDisk = (PCREATE_VIRTUAL_DISK)GetProcAddress(DllVirtDisk.hDll, "CreateVirtualDisk");
    DllVirtDisk.pDetachVirtualDisk = (PDETACH_VIRTUAL_DISK)GetProcAddress(DllVirtDisk.hDll, "DetachVirtualDisk");
    DllVirtDisk.pExpandVirtualDisk = (PEXPAND_VIRTUAL_DISK)GetProcAddress(DllVirtDisk.hDll, "ExpandVirtualDisk");
    DllVirtDisk.pGetVirtualDiskPhysicalPath = (PGET_VIRTUAL_DISK_PHYSICAL_PATH)GetProcAddress(DllVirtDisk.hDll, "GetVirtualDiskPhysicalPath");
    DllVirtDisk.pOpenVirtualDisk = (POPEN_VIRTUAL_DISK)GetProcAddress(DllVirtDisk.hDll, "OpenVirtualDisk");
    DllVirtDisk.pMergeVirtualDisk = (PMERGE_VIRTUAL_DISK)GetProcAddress(DllVirtDisk.hDll, "MergeVirtualDisk");
    DllVirtDisk.pResizeVirtualDisk = (PRESIZE_VIRTUAL_DISK)GetProcAddress(DllVirtDisk.hDll, "ResizeVirtualDisk");

    return TRUE;
}

/**
 A structure containing pointers to winbrand.dll functions that can be used if
 they are found but programs do not have a hard dependency on.
 */
YORI_WINBRAND_FUNCTIONS DllWinBrand;

/**
 Load pointers to all optional WinBrand.dll functions.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriLibLoadWinBrandFunctions(VOID)
{

    if (DllWinBrand.hDll != NULL) {
        return TRUE;
    }

    DllWinBrand.hDll = YoriLibLoadLibraryFromSystemDirectory(_T("WINBRAND.DLL"));
    if (DllWinBrand.hDll == NULL) {
        return FALSE;
    }

    DllWinBrand.pBrandingFormatString = (PBRANDING_FORMAT_STRING)GetProcAddress(DllWinBrand.hDll, "BrandingFormatString");
    return TRUE;
}

/**
 A structure containing pointers to wtsapi32.dll functions that can be used if
 they are found but programs do not have a hard dependency on.
 */
YORI_WTSAPI32_FUNCTIONS DllWtsApi32;

/**
 Load pointers to all optional WtsApi32.dll functions.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriLibLoadWtsApi32Functions(VOID)
{

    if (DllWtsApi32.hDll != NULL) {
        return TRUE;
    }

    DllWtsApi32.hDll = YoriLibLoadLibraryFromSystemDirectory(_T("WTSAPI32.DLL"));
    if (DllWtsApi32.hDll == NULL) {
        return FALSE;
    }

    DllWtsApi32.pWTSDisconnectSession = (PWTS_DISCONNECT_SESSION)GetProcAddress(DllWtsApi32.hDll, "WTSDisconnectSession");

    return TRUE;
}


// vim:sw=4:ts=4:et:


<|start_filename|>lib/filecomp.c<|end_filename|>
/**
 * @file lib/filecomp.c
 *
 * Yori lib perform transparent individual file compression on background
 * threads
 *
 * Copyright (c) 2018 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, COPYESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 A single item to compress or decompress.
 */
typedef struct _YORILIB_PENDING_ACTION {

    /**
     A list of files requiring compression.
     */
    YORI_LIST_ENTRY CompressList;

    /**
     The file name to compress.
     */
    YORI_STRING FileName;

    /**
     If the file should be compressed, set to TRUE.  If the file should be
     decompressed, set to FALSE.
     */
    BOOL Compress;

} YORILIB_PENDING_ACTION, *PYORILIB_PENDING_ACTION;

/**
 Set up the compress context to contain support for the compression thread pool.

 @param CompressContext Pointer to the compress context.

 @param CompressionAlgorithm The compression algorithm to use for this set of
        compressed files.

 @return TRUE if the context was successfully initialized for compression,
         FALSE if it was not.
 */
BOOL
YoriLibInitializeCompressContext(
    __in PYORILIB_COMPRESS_CONTEXT CompressContext,
    __in YORILIB_COMPRESS_ALGORITHM CompressionAlgorithm
    )
{
    SYSTEM_INFO SystemInfo;
    GetSystemInfo(&SystemInfo);

    CompressContext->CompressionAlgorithm = CompressionAlgorithm;

    //
    //  Create threads equal to the number of CPUs.  The system can compress
    //  chunks of data on background threads, so this is just the number of
    //  threads initiating work.  Unfortunately, the call to CreateFile
    //  after copy has a tendency to block, so we need this to be part of
    //  the threadpool to prevent bottlenecking the copy.
    //

    CompressContext->MaxThreads = SystemInfo.dwNumberOfProcessors;
    if (CompressContext->MaxThreads < 1) {
        CompressContext->MaxThreads = 1;
    }
    if (CompressContext->MaxThreads > 32) {
        CompressContext->MaxThreads = 32;
    }

    YoriLibInitializeListHead(&CompressContext->PendingList);
    CompressContext->WorkerWaitEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
    if (CompressContext->WorkerWaitEvent == NULL) {
        return FALSE;
    }

    CompressContext->WorkerShutdownEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
    if (CompressContext->WorkerShutdownEvent == NULL) {
        return FALSE;
    }

    CompressContext->Mutex = CreateMutex(NULL, FALSE, NULL);
    if (CompressContext->Mutex == NULL) {
        return FALSE;
    }

    CompressContext->Threads = YoriLibMalloc(sizeof(HANDLE) * CompressContext->MaxThreads);
    if (CompressContext->Threads == NULL) {
        return FALSE;
    }

    return TRUE;
}

/**
 Free the internal allocations and state of a compress context.  This
 also includes waiting for all outstanding compression tasks to complete.
 Note the CompressContext allocation itself is not freed, since this is
 typically on the stack.

 @param CompressContext Pointer to the compress context to clean up.
 */
VOID
YoriLibFreeCompressContext(
    __in PYORILIB_COMPRESS_CONTEXT CompressContext
    )
{
    if (CompressContext->ThreadsAllocated > 0) {
        DWORD Index;
        SetEvent(CompressContext->WorkerShutdownEvent);
        WaitForMultipleObjects(CompressContext->ThreadsAllocated, CompressContext->Threads, TRUE, INFINITE);
        for (Index = 0; Index < CompressContext->ThreadsAllocated; Index++) {
            CloseHandle(CompressContext->Threads[Index]);
            CompressContext->Threads[Index] = NULL;
        }
        ASSERT(YoriLibIsListEmpty(&CompressContext->PendingList));
    }
    if (CompressContext->WorkerWaitEvent != NULL) {
        CloseHandle(CompressContext->WorkerWaitEvent);
        CompressContext->WorkerWaitEvent = NULL;
    }
    if (CompressContext->WorkerShutdownEvent != NULL) {
        CloseHandle(CompressContext->WorkerShutdownEvent);
        CompressContext->WorkerShutdownEvent = NULL;
    }
    if (CompressContext->Mutex != NULL) {
        CloseHandle(CompressContext->Mutex);
        CompressContext->Mutex = NULL;
    }
    if (CompressContext->Threads != NULL) {
        YoriLibFree(CompressContext->Threads);
        CompressContext->Threads = NULL;
    }
}

/**
 Compress a single file.  This can be called on worker threads, or occasionally
 on the main thread if the worker threads are backlogged.

 @param PendingAction Pointer to the object that needs to be compressed.
        This structure is deallocated within this function.

 @param CompressionAlgorithm Specifies the compression algorithm to compress
        the file with.
 
 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriLibCompressSingleFile(
    __in PYORILIB_PENDING_ACTION PendingAction,
    __in YORILIB_COMPRESS_ALGORITHM CompressionAlgorithm
    )
{
    HANDLE DestFileHandle;
    DWORD AccessRequired;
    BY_HANDLE_FILE_INFORMATION FileInfo;
    DWORD BytesReturned;
    BOOL Result = FALSE;
    BOOL CompressFile = TRUE;

    //
    //  In order to compress system files, we can't open for write access.
    //  WOF doesn't require write access, although NTFS does.
    //

    AccessRequired = FILE_READ_DATA | FILE_READ_ATTRIBUTES | SYNCHRONIZE;
    if (CompressionAlgorithm.NtfsAlgorithm != 0) {
        AccessRequired |= FILE_WRITE_DATA | FILE_WRITE_ATTRIBUTES;
    }


    DestFileHandle = CreateFile(PendingAction->FileName.StartOfString,
                                AccessRequired,
                                FILE_SHARE_READ|FILE_SHARE_DELETE,
                                NULL,
                                OPEN_EXISTING,
                                FILE_FLAG_BACKUP_SEMANTICS,
                                NULL);

    if (DestFileHandle == INVALID_HANDLE_VALUE) {
        YoriLibFree(PendingAction);
        return FALSE;
    }

    //
    //  File system compression works by storing the data in fewer allocation
    //  units.  What this means is for files that are very small the
    //  possibility and quantity of allocation units reclaimed can't justify
    //  the overhead, so just skip them.
    //

    if (!GetFileInformationByHandle(DestFileHandle, &FileInfo)) {
        goto Exit;
    }

    if (FileInfo.nFileSizeHigh == 0 &&
        FileInfo.nFileSizeLow < 10 * 1024) {

        goto Exit;
    }


    if (CompressionAlgorithm.NtfsAlgorithm != 0) {
        USHORT Algorithm = (USHORT)CompressionAlgorithm.NtfsAlgorithm;

        Result = DeviceIoControl(DestFileHandle,
                                 FSCTL_SET_COMPRESSION,
                                 &Algorithm,
                                 sizeof(Algorithm),
                                 NULL,
                                 0,
                                 &BytesReturned,
                                 NULL);

    } else {
        struct {
            WOF_EXTERNAL_INFO WofInfo;
            FILE_PROVIDER_EXTERNAL_INFO FileInfo;
        } CompressInfo;

        ZeroMemory(&CompressInfo, sizeof(CompressInfo));

        Result = DeviceIoControl(DestFileHandle,
                                 FSCTL_GET_EXTERNAL_BACKING,
                                 NULL,
                                 0,
                                 &CompressInfo,
                                 sizeof(CompressInfo),
                                 &BytesReturned,
                                 NULL);

        if (Result) {
            if (CompressInfo.WofInfo.Version == 1 &&
                CompressInfo.WofInfo.Provider == WOF_PROVIDER_FILE &&
                CompressInfo.FileInfo.Version == 1 &&
                CompressInfo.FileInfo.Algorithm == CompressionAlgorithm.WofAlgorithm) {

                CompressFile = FALSE;
            }
        }

        if (CompressFile) {
            ZeroMemory(&CompressInfo, sizeof(CompressInfo));
            CompressInfo.WofInfo.Version = 1;
            CompressInfo.WofInfo.Provider = WOF_PROVIDER_FILE;
            CompressInfo.FileInfo.Version = 1;
            CompressInfo.FileInfo.Algorithm = CompressionAlgorithm.WofAlgorithm;

            Result = DeviceIoControl(DestFileHandle,
                                     FSCTL_SET_EXTERNAL_BACKING,
                                     &CompressInfo,
                                     sizeof(CompressInfo),
                                     NULL,
                                     0,
                                     &BytesReturned,
                                     NULL);
        }
    }

Exit:
    if (DestFileHandle != NULL) {
        CloseHandle(DestFileHandle);
    }
    YoriLibFree(PendingAction);
    return Result;
}

/**
 Decompress a single file.  This can be called on worker threads, or
 occasionally on the main thread if the worker threads are backlogged.

 @param PendingAction Pointer to the object that needs to be decompressed.
        This structure is deallocated within this function.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriLibDecompressSingleFile(
    __in PYORILIB_PENDING_ACTION PendingAction
    )
{
    BOOL GlobalResult = TRUE;
    DWORD BytesReturned;
    DWORD AccessRequired;
    BOOL LocalResult;
    USHORT Algorithm = 0;
    HANDLE DestFileHandle;

    //
    //  Normally WOF will decompress if a file is opened for FILE_WRITE_DATA
    //  and NTFS requires it to attempt decompression
    //

    AccessRequired = FILE_READ_DATA | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES | SYNCHRONIZE | FILE_WRITE_DATA;

    DestFileHandle = CreateFile(PendingAction->FileName.StartOfString,
                                AccessRequired,
                                FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
                                NULL,
                                OPEN_EXISTING,
                                FILE_FLAG_BACKUP_SEMANTICS,
                                NULL);

    if (DestFileHandle == INVALID_HANDLE_VALUE) {

        DWORD Error;

        Error = GetLastError();

        // 
        //  If we can't get a handle with FILE_WRITE_DATA, try without write
        //  access.  This is to allow decompression of system files, which
        //  can be performed with FSCTL_DELETE_EXTERNAL_BACKING .
        //

        if (Error == ERROR_ACCESS_DENIED || Error == ERROR_SHARING_VIOLATION) {

            AccessRequired = FILE_READ_DATA | FILE_READ_ATTRIBUTES | SYNCHRONIZE;

            DestFileHandle = CreateFile(PendingAction->FileName.StartOfString,
                                        AccessRequired,
                                        FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
                                        NULL,
                                        OPEN_EXISTING,
                                        FILE_FLAG_BACKUP_SEMANTICS,
                                        NULL);
        }
    }

    if (DestFileHandle == INVALID_HANDLE_VALUE) {
        YoriLibFree(PendingAction);
        return FALSE;
    }

    if ((AccessRequired & FILE_WRITE_DATA) != 0) {
        LocalResult = DeviceIoControl(DestFileHandle,
                                      FSCTL_SET_COMPRESSION,
                                      &Algorithm,
                                      sizeof(Algorithm),
                                      NULL,
                                      0,
                                      &BytesReturned,
                                      NULL);

        if (!LocalResult) {
            GlobalResult = FALSE;
        }
    } else {

        //
        //  If we can't decompress NTFS due to no access, check whether the
        //  file required decompression.  If it didn't, this can be
        //  treated as success.
        //

        LocalResult = DeviceIoControl(DestFileHandle,
                                      FSCTL_GET_COMPRESSION,
                                      &Algorithm,
                                      sizeof(Algorithm),
                                      NULL,
                                      0,
                                      &BytesReturned,
                                      NULL);

        if (!LocalResult) {
            GlobalResult = FALSE;
        } else if (Algorithm != 0) {
            GlobalResult = FALSE;
        }
    }

    LocalResult = DeviceIoControl(DestFileHandle,
                                  FSCTL_DELETE_EXTERNAL_BACKING,
                                  NULL,
                                  0,
                                  NULL,
                                  0,
                                  &BytesReturned,
                                  NULL);

    if (!LocalResult) {
        GlobalResult = FALSE;
    }

    CloseHandle(DestFileHandle);
    YoriLibFree(PendingAction);
    return GlobalResult;
}


/**
 A background thread which will attempt to compress any items that it finds on
 a list of files requiring compression.

 @param Context Pointer to the compress context.

 @return TRUE to indicate success, FALSE to indicate one or more compression
         operations failed.
 */
DWORD WINAPI
YoriLibCompressWorker(
    __in LPVOID Context
    )
{
    PYORILIB_COMPRESS_CONTEXT CompressContext = (PYORILIB_COMPRESS_CONTEXT)Context;
    DWORD FoundEvent;
    PYORILIB_PENDING_ACTION PendingAction;
    BOOL Result = TRUE;

    while (TRUE) {

        //
        //  Wait for an indication of more work or shutdown.
        //

        FoundEvent = WaitForMultipleObjects(2, &CompressContext->WorkerWaitEvent, FALSE, INFINITE);

        //
        //  Process any queued work.
        //

        while (TRUE) {
            WaitForSingleObject(CompressContext->Mutex, INFINITE);
            if (!YoriLibIsListEmpty(&CompressContext->PendingList)) {
                PendingAction = CONTAINING_RECORD(CompressContext->PendingList.Next, YORILIB_PENDING_ACTION, CompressList);
                ASSERT(CompressContext->ItemsQueued > 0);
                CompressContext->ItemsQueued--;
                YoriLibRemoveListItem(&PendingAction->CompressList);
                ReleaseMutex(CompressContext->Mutex);

                if (PendingAction->Compress) {
                    if (!YoriLibCompressSingleFile(PendingAction, CompressContext->CompressionAlgorithm)) {
                        Result = FALSE;
                    }
                } else {
                    if (!YoriLibDecompressSingleFile(PendingAction)) {
                        Result = FALSE;
                    }
                }

            } else {
                ASSERT(CompressContext->ItemsQueued == 0);
                ReleaseMutex(CompressContext->Mutex);
                break;
            }
        }

        //
        //  If shutdown was requested, terminate the thread.
        //

        if (FoundEvent == (WAIT_OBJECT_0 + 1)) {
            break;
        }
    }

    return Result;
}

/**
 Add a pending action to the queue of items to be performed by background
 threads.  If the background threads already have an excessively large
 queue of work, this function returns FALSE to indicate it should be
 completed by the foreground thread.

 @param CompressContext Pointer to the compress context describing the state
        of background threads.

 @param PendingAction Pointer to the action to perform.

 @return TRUE if the action was queued to be processed by background threads,
         or FALSE if it should be completed by the foreground thread.
 */
BOOL
YoriLibAddToBackgroundCompressQueue(
    __in PYORILIB_COMPRESS_CONTEXT CompressContext,
    __in PYORILIB_PENDING_ACTION PendingAction
    )
{
    BOOL Result = FALSE;
    DWORD ThreadId;

    WaitForSingleObject(CompressContext->Mutex, INFINITE);
    if (CompressContext->ThreadsAllocated == 0 ||
        (CompressContext->ItemsQueued > CompressContext->ThreadsAllocated * 2 &&
         CompressContext->ThreadsAllocated < CompressContext->MaxThreads)) {

        CompressContext->Threads[CompressContext->ThreadsAllocated] = CreateThread(NULL, 0, YoriLibCompressWorker, CompressContext, 0, &ThreadId);
        if (CompressContext->Threads[CompressContext->ThreadsAllocated] != NULL) {
            CompressContext->ThreadsAllocated++;
            if (CompressContext->Verbose) {
                YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Created compression thread %i\n"), CompressContext->ThreadsAllocated);
            }
        }
    }

    if (CompressContext->ThreadsAllocated > 0 &&
        CompressContext->ItemsQueued < CompressContext->MaxThreads * 2) {

        YoriLibAppendList(&CompressContext->PendingList, &PendingAction->CompressList);
        CompressContext->ItemsQueued++;
        Result = TRUE;
    }

    ReleaseMutex(CompressContext->Mutex);

    SetEvent(CompressContext->WorkerWaitEvent);
    return Result;
}

/**
 Compress a given file with a specified algorithm.  This routine will skip
 small files that do not benefit from compression.

 @param CompressContext Pointer to the compress context specifying where to
        queue compression tasks and which compression algorithm to use.

 @param FileName Pointer to the file name to compress.

 @return TRUE to indicate the file was successfully compressed, FALSE if it
         was not.
 */
BOOL
YoriLibCompressFileInBackground(
    __in PYORILIB_COMPRESS_CONTEXT CompressContext,
    __in PYORI_STRING FileName
    )
{
    PYORILIB_PENDING_ACTION PendingAction;
    BOOL Result = FALSE;

    ASSERT(YoriLibIsStringNullTerminated(FileName));

    PendingAction = YoriLibMalloc(sizeof(YORILIB_PENDING_ACTION) + (FileName->LengthInChars + 1) * sizeof(TCHAR));
    if (PendingAction == NULL) {
        goto Exit;
    }
    PendingAction->Compress = TRUE;
    YoriLibInitEmptyString(&PendingAction->FileName);
    PendingAction->FileName.StartOfString = (LPTSTR)(PendingAction + 1);
    PendingAction->FileName.LengthInChars = FileName->LengthInChars;
    PendingAction->FileName.LengthAllocated = FileName->LengthInChars + 1;
    memcpy(PendingAction->FileName.StartOfString, FileName->StartOfString, (FileName->LengthInChars + 1) * sizeof(TCHAR));

    if (YoriLibAddToBackgroundCompressQueue(CompressContext, PendingAction)) {
        PendingAction = NULL;
    }

    Result = TRUE;

    //
    //  If the threads in the pool are all busy (we have too many items
    //  waiting) do the compression on the main thread.  This is mainly done to
    //  prevent the main thread from continuing to pile in more items that the
    //  pool can't get to.
    //

    if (PendingAction != NULL) {
        if (CompressContext->Verbose) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Compressing %y on main thread for back pressure\n"), FileName);
        }
        if (!YoriLibCompressSingleFile(PendingAction, CompressContext->CompressionAlgorithm)) {
            Result = FALSE;
        }
    }

Exit:
    return Result;
}

/**
 Decompress a given file.

 @param CompressContext Pointer to the compress context specifying where to
        queue compression tasks.

 @param FileName Pointer to the file name to compress.

 @return TRUE to indicate the file was successfully queued for decompression,
         FALSE if it was not.
 */
BOOL
YoriLibDecompressFileInBackground(
    __in PYORILIB_COMPRESS_CONTEXT CompressContext,
    __in PYORI_STRING FileName
    )
{
    PYORILIB_PENDING_ACTION PendingAction;
    BOOL Result = FALSE;

    ASSERT(YoriLibIsStringNullTerminated(FileName));

    PendingAction = YoriLibMalloc(sizeof(YORILIB_PENDING_ACTION) + (FileName->LengthInChars + 1) * sizeof(TCHAR));
    if (PendingAction == NULL) {
        goto Exit;
    }
    PendingAction->Compress = FALSE;
    YoriLibInitEmptyString(&PendingAction->FileName);
    PendingAction->FileName.StartOfString = (LPTSTR)(PendingAction + 1);
    PendingAction->FileName.LengthInChars = FileName->LengthInChars;
    PendingAction->FileName.LengthAllocated = FileName->LengthInChars + 1;
    memcpy(PendingAction->FileName.StartOfString, FileName->StartOfString, (FileName->LengthInChars + 1) * sizeof(TCHAR));

    if (YoriLibAddToBackgroundCompressQueue(CompressContext, PendingAction)) {
        PendingAction = NULL;
    }

    Result = TRUE;

    //
    //  If the threads in the pool are all busy (we have too many items
    //  waiting) do the decompression on the main thread.  This is mainly done
    //  to prevent the main thread from continuing to pile in more items that
    //  the pool can't get to.
    //

    if (PendingAction != NULL) {
        if (CompressContext->Verbose) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Decompressing %y on main thread for back pressure\n"), FileName);
        }
        if (!YoriLibDecompressSingleFile(PendingAction)) {
            Result = FALSE;
        }
    }

Exit:
    return Result;
}

/**
 Return the version of WOF available on a specified path.  This will return
 zero if WOF is not attached to the path, or does not support individual file
 compression.

 @param FileName The file path to check for WOF support.

 @return The version of WOF, in GetVersion form.  Zero if WOF is not
         available.
 */
DWORD
YoriLibGetWofVersionAvailable(
    __in PYORI_STRING FileName
    )
{
    HANDLE FileHandle;
    DWORD AccessRequired;
    DWORD WofVersion;
    DWORD BytesReturned;
    WOF_EXTERNAL_INFO WofInfo;

    ASSERT(YoriLibIsStringNullTerminated(FileName));

    AccessRequired = FILE_READ_ATTRIBUTES | SYNCHRONIZE;

    FileHandle = CreateFile(FileName->StartOfString,
                            AccessRequired,
                            FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
                            NULL,
                            OPEN_EXISTING,
                            FILE_FLAG_BACKUP_SEMANTICS,
                            NULL);

    if (FileHandle == INVALID_HANDLE_VALUE) {
        return 0;
    }

    WofInfo.Version = 1;
    WofInfo.Provider = WOF_PROVIDER_FILE;

    WofVersion = 0;
    DeviceIoControl(FileHandle,
                    FSCTL_GET_WOF_VERSION,
                    &WofInfo,
                    sizeof(WofInfo),
                    &WofVersion,
                    sizeof(WofVersion),
                    &BytesReturned,
                    NULL);

    CloseHandle(FileHandle);
    return WofVersion;
}


// vim:sw=4:ts=4:et:


<|start_filename|>crt/bigmul.asm<|end_filename|>
;
; BIGMUL.ASM
;
; Implementation for for signed and unsigned multiplication of a 64 bit integer
; by a 64 bit integer.
;
; Copyright (c) 2017 <NAME>
;
; Permission is hereby granted, free of charge, to any person obtaining a copy
; of this software and associated documentation files (the "Software"), to deal
; in the Software without restriction, including without limitation the rights
; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
; copies of the Software, and to permit persons to whom the Software is
; furnished to do so, subject to the following conditions:
;
; The above copyright notice and this permission notice shall be included in
; all copies or substantial portions of the Software.
;
; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
; THE SOFTWARE.
;

.386
.MODEL FLAT, C
.CODE

; LARGE_INTEGER [High EDX, Low EAX]
; _allmul(
;     LARGE_INTEGER Value1, [High ESP + 8, Low ESP + 4],
;     LARGE_INTEGER Value2 [High ESP + 16, Low ESP + 12]
; );

; Multiply low by high of both components, adding them into a temporary
; register.  Multiply low by low, which might result in a high component,
; and add back the temporary result into this high component.

_allmul proc

push ebx

mov eax, [esp + 8]
mov edx, [esp + 20]
mul edx
mov ebx, eax

mov eax, [esp + 16]
mov edx, [esp + 12]
mul edx
add ebx, eax

mov eax, [esp + 8]
mov edx, [esp + 16]
mul edx
add edx, ebx

pop ebx

ret 16
_allmul endp

END


<|start_filename|>ypm/upgrade.c<|end_filename|>
/**
 * @file ypm/upgrade.c
 *
 * Yori shell package manager upgrade installed packages
 *
 * Copyright (c) 2018-2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>
#include <yoripkg.h>
#include "ypm.h"

/**
 Help text to display to the user.
 */
const
CHAR strYpmUpgradeHelpText[] =
        "\n"
        "Upgrade installed packages.\n"
        "\n"
        "YPM [-license]\n"
        "YPM [-a <arch>] -u|-ud|-us [<pkg>]\n"
        "\n"
        "   -a             Specify a CPU architecture to upgrade to\n"
        "   -u             Upgrade to latest version of the same type\n"
        "   -ud            Upgrade to latest daily version\n"
        "   -us            Upgrade to latest stable version\n";


/**
 Display usage text to the user.
 */
BOOL
YpmUpgradeHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Ypm %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strYpmUpgradeHelpText);
    return TRUE;
}

/**
 Upgrade installed packages.

 @param UpgradePrefer Specifies if the upgrade should maintain the current
        state of packages, or prefer latest stable packages, or prefer latest
        daily packages.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process.
 */
DWORD
YpmUpgradeInternal(
    __in YORIPKG_UPGRADE_PREFER UpgradePrefer,
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    YORI_STRING Arg;
    PYORI_STRING NewArch = NULL;

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                YpmUpgradeHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2017-2021"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("a")) == 0) {
                if (i + 1 < ArgC) {
                    NewArch = &ArgV[i + 1];
                    i++;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                ArgumentUnderstood = TRUE;
                StartArg = i + 1;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    if (StartArg == 0 || StartArg >= ArgC) {
        YoriPkgUpgradeInstalledPackages(UpgradePrefer, NewArch);
    } else {
        for (i = StartArg; i < ArgC; i++) {
            YoriPkgUpgradeSinglePackage(&ArgV[i], UpgradePrefer, NewArch);
        }
    }

    return EXIT_SUCCESS;
}

/**
 Upgrade installed packages.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process.
 */
DWORD
YpmUpgrade(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    return YpmUpgradeInternal(YoriPkgUpgradePreferSame, ArgC, ArgV);
}

/**
 Upgrade installed packages to the latest daily version.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process.
 */
DWORD
YpmUpgradePreferDaily(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    return YpmUpgradeInternal(YoriPkgUpgradePreferDaily, ArgC, ArgV);
}

/**
 Upgrade installed packages to the latest stable version.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process.
 */
DWORD
YpmUpgradePreferStable(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    return YpmUpgradeInternal(YoriPkgUpgradePreferStable, ArgC, ArgV);
}

// vim:sw=4:ts=4:et:


<|start_filename|>clmp/Makefile<|end_filename|>

BINARIES=clmp.exe

!INCLUDE "..\config\common.mk"

!IF $(PDB)==1
LINKPDB=/Pdb:clmp.pdb
!ENDIF

OBJS=\
	 clmp.obj        \

compile: $(OBJS)

clmp.exe: $(OBJS) $(YORILIB) $(YORICRT) $(YORIVER)
	@echo $@
	@$(LINK) $(LDFLAGS) -entry:$(YENTRY) $(OBJS) $(LIBS) $(CRTLIB) $(YORILIB) $(YORIVER) -version:$(YORI_VER_MAJOR).$(YORI_VER_MINOR) $(LINKPDB) -out:$@



<|start_filename|>erase/erase.c<|end_filename|>
/**
 * @file erase/erase.c
 *
 * Yori shell erase files
 *
 * Copyright (c) 2017-2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS ERASE A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE ERASE ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 Help text to display to the user.
 */
const
CHAR strEraseHelpText[] =
        "\n"
        "Delete one or more files.\n"
        "\n"
        "ERASE [-license] [-b] [-p | -r] [-s] <file> [<file>...]\n"
        "\n"
        "   --             Treat all further arguments as files to delete\n"
        "   -b             Use basic search criteria for files only\n"
        "   -p             Delete files with POSIX semantics\n"
        "   -r             Send files to the recycle bin\n"
        "   -s             Erase all files matching the pattern in all subdirectories\n";

/**
 Display usage text to the user.
 */
BOOL
EraseHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Erase %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strEraseHelpText);
    return TRUE;
}

/**
 A structure passed to each file found.
 */
typedef struct _ERASE_CONTEXT {

    /**
     TRUE if files should be deleted with POSIX semantics.
     */
    BOOLEAN PosixSemantics;

    /**
     TRUE if files should be sent to the recycle bin.
     */
    BOOLEAN RecycleBin;

    /**
     The number of files found.
     */
    DWORDLONG FilesFound;

    /**
     The number of files successfully marked for delete.
     */
    DWORDLONG FilesMarkedForDelete;

} ERASE_CONTEXT, *PERASE_CONTEXT;

/**
 Delete a file via DeleteFile or via the POSIX delete API depending on which
 command line arguments were specified.

 @param EraseContext Pointer to the context indicating which delete mode to
        use.

 @param FileName Pointer to the file name to delete.

 @return TRUE to indicate the file was marked for delete successfully, or
         FALSE to indicate failure.
 */
BOOL
EraseDeleteFile(
    __in PERASE_CONTEXT EraseContext,
    __in PYORI_STRING FileName
    )
{
    if (!EraseContext->PosixSemantics) {
        return DeleteFile(FileName->StartOfString);
    }

    return YoriLibPosixDeleteFile(FileName);
}

/**
 A callback that is invoked when a file is found that matches a search criteria
 specified in the set of strings to enumerate.

 @param FilePath Pointer to the file path that was found.

 @param FileInfo Information about the file.

 @param Depth Recursion depth, ignored in this application.

 @param Context Specifies if erase should move objects to the recycle bin and
        records the count of files found.

 @return TRUE to continute enumerating, FALSE to abort.
 */
BOOL
EraseFileFoundCallback(
    __in PYORI_STRING FilePath,
    __in PWIN32_FIND_DATA FileInfo,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    DWORD Err;
    LPTSTR ErrText;
    BOOLEAN FileDeleted;
    PERASE_CONTEXT EraseContext = (PERASE_CONTEXT)Context;

    UNREFERENCED_PARAMETER(Depth);

    ASSERT(YoriLibIsStringNullTerminated(FilePath));

    FileDeleted = FALSE;
    if ((FileInfo->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) {

        EraseContext->FilesFound++;

        //
        //  If the user wanted it deleted via the recycle bin, try that.
        //

        if (EraseContext->RecycleBin) {
            if (YoriLibRecycleBinFile(FilePath)) {
                FileDeleted = TRUE;
            }
        }

        //
        //  If the user didn't ask for recycle bin or if that failed, delete
        //  directly.
        //

        if (!FileDeleted && !EraseDeleteFile(EraseContext, FilePath)) {
            Err = GetLastError();
            if (Err == ERROR_ACCESS_DENIED) {
                DWORD OldAttributes = GetFileAttributes(FilePath->StartOfString);
                DWORD NewAttributes = OldAttributes & ~(FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM);

                if (OldAttributes != NewAttributes) {
                    SetFileAttributes(FilePath->StartOfString, NewAttributes);

                    Err = NO_ERROR;

                    if (!EraseDeleteFile(EraseContext, FilePath)) {
                        Err = GetLastError();
                    } else {
                        FileDeleted = TRUE;
                    }

                    if (Err != NO_ERROR) {
                        SetFileAttributes(FilePath->StartOfString, OldAttributes);
                    }
                }
            }

            if (Err != NO_ERROR) {
                ErrText = YoriLibGetWinErrorText(Err);
                YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("erase: delete of %y failed: %s"), FilePath, ErrText);
                YoriLibFreeWinErrorText(ErrText);
            }
        } else {
            FileDeleted = TRUE;
        }

        if (FileDeleted) {
            EraseContext->FilesMarkedForDelete++;
        }
    }
    return TRUE;
}

/**
 A callback that is invoked when a directory cannot be successfully enumerated.

 @param FilePath Pointer to the file path that could not be enumerated.

 @param ErrorCode The Win32 error code describing the failure.

 @param Depth Recursion depth, ignored in this application.

 @param Context Context, ignored in this function.

 @return TRUE to continute enumerating, FALSE to abort.
 */
BOOL
EraseFileEnumerateErrorCallback(
    __in PYORI_STRING FilePath,
    __in DWORD ErrorCode,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    YORI_STRING UnescapedFilePath;
    BOOL Result = FALSE;

    UNREFERENCED_PARAMETER(Depth);
    UNREFERENCED_PARAMETER(Context);

    YoriLibInitEmptyString(&UnescapedFilePath);
    if (!YoriLibUnescapePath(FilePath, &UnescapedFilePath)) {
        UnescapedFilePath.StartOfString = FilePath->StartOfString;
        UnescapedFilePath.LengthInChars = FilePath->LengthInChars;
    }

    if (ErrorCode == ERROR_FILE_NOT_FOUND || ErrorCode == ERROR_PATH_NOT_FOUND) {
        Result = TRUE;
    } else {
        LPTSTR ErrText = YoriLibGetWinErrorText(ErrorCode);
        YORI_STRING DirName;
        LPTSTR FilePart;
        YoriLibInitEmptyString(&DirName);
        DirName.StartOfString = UnescapedFilePath.StartOfString;
        FilePart = YoriLibFindRightMostCharacter(&UnescapedFilePath, '\\');
        if (FilePart != NULL) {
            DirName.LengthInChars = (DWORD)(FilePart - DirName.StartOfString);
        } else {
            DirName.LengthInChars = UnescapedFilePath.LengthInChars;
        }
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Enumerate of %y failed: %s"), &DirName, ErrText);
        YoriLibFreeWinErrorText(ErrText);
    }
    YoriLibFreeStringContents(&UnescapedFilePath);
    return Result;
}


#ifdef YORI_BUILTIN
/**
 The main entrypoint for the erase builtin command.
 */
#define ENTRYPOINT YoriCmd_YERASE
#else
/**
 The main entrypoint for the erase standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the erase cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the child process on success, or failure if the child
         could not be launched.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD MatchFlags;
    BOOL Recursive;
    BOOL BasicEnumeration;
    DWORD StartArg = 0;
    DWORD i;
    ERASE_CONTEXT Context;
    YORI_STRING Arg;

    ZeroMemory(&Context, sizeof(Context));
    Recursive = FALSE;
    BasicEnumeration = FALSE;

    for (i = 1; i < ArgC; i++) {

        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));
        ArgumentUnderstood = FALSE;

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                EraseHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2017-2021"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("b")) == 0) {
                BasicEnumeration = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("p")) == 0) {
                Context.PosixSemantics = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("r")) == 0) {
                Context.RecycleBin = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("s")) == 0) {
                Recursive = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                ArgumentUnderstood = TRUE;
                StartArg = i + 1;
                break;
            }

        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    if (StartArg == 0) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("erase: missing argument\n"));
        return EXIT_FAILURE;
    }

    if (Context.PosixSemantics &&
        DllKernel32.pSetFileInformationByHandle == NULL) {

        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("erase: OS support not present\n"));
        return EXIT_FAILURE;
    }

#if YORI_BUILTIN
    YoriLibCancelEnable();
#endif

    //
    //  Attempt to enable backup privilege so an administrator can access more
    //  objects successfully.
    //

    YoriLibEnableBackupPrivilege();

    MatchFlags = YORILIB_FILEENUM_RETURN_FILES | YORILIB_FILEENUM_DIRECTORY_CONTENTS;
    if (Recursive) {
        MatchFlags |= YORILIB_FILEENUM_RECURSE_BEFORE_RETURN | YORILIB_FILEENUM_RECURSE_PRESERVE_WILD;
    }
    if (BasicEnumeration) {
        MatchFlags |= YORILIB_FILEENUM_BASIC_EXPANSION;
    }

    for (i = StartArg; i < ArgC; i++) {

        YoriLibForEachStream(&ArgV[i],
                             MatchFlags,
                             0,
                             EraseFileFoundCallback,
                             EraseFileEnumerateErrorCallback,
                             &Context);
    }

    if (Context.FilesFound == 0) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("erase: no matching files found\n"));
        ASSERT(Context.FilesMarkedForDelete == 0);
    }

    if (Context.FilesMarkedForDelete == 0) {
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>lib/dyld_net.c<|end_filename|>
/**
 * @file lib/dyld.c
 *
 * Yori dynamically loaded OS function support for HTTP functions
 *
 * Copyright (c) 2018-2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "yoripch.h"
#include "yorilib.h"

/**
 A structure containing pointers to winhttp.dll functions that can be used if
 they are found but programs do not have a hard dependency on.
 */
YORI_WINHTTP_FUNCTIONS DllWinHttp;

/**
 Load pointers to all optional WinHttp.dll functions.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriLibLoadWinHttpFunctions(VOID)
{

    if (DllWinHttp.hDll != NULL) {
        return TRUE;
    }

    DllWinHttp.hDll = YoriLibLoadLibraryFromSystemDirectory(_T("WINHTTP.DLL"));
    if (DllWinHttp.hDll == NULL) {
        return FALSE;
    }

    DllWinHttp.pWinHttpCloseHandle = (PWIN_HTTP_CLOSE_HANDLE)GetProcAddress(DllWinHttp.hDll, "WinHttpCloseHandle");
    DllWinHttp.pWinHttpConnect = (PWIN_HTTP_CONNECT)GetProcAddress(DllWinHttp.hDll, "WinHttpConnect");
    DllWinHttp.pWinHttpOpen = (PWIN_HTTP_OPEN)GetProcAddress(DllWinHttp.hDll, "WinHttpOpen");
    DllWinHttp.pWinHttpOpenRequest = (PWIN_HTTP_OPEN_REQUEST)GetProcAddress(DllWinHttp.hDll, "WinHttpOpenRequest");
    DllWinHttp.pWinHttpQueryHeaders = (PWIN_HTTP_QUERY_HEADERS)GetProcAddress(DllWinHttp.hDll, "WinHttpQueryHeaders");
    DllWinHttp.pWinHttpReadData = (PWIN_HTTP_READ_DATA)GetProcAddress(DllWinHttp.hDll, "WinHttpReadData");
    DllWinHttp.pWinHttpReceiveResponse = (PWIN_HTTP_RECEIVE_RESPONSE)GetProcAddress(DllWinHttp.hDll, "WinHttpReceiveResponse");
    DllWinHttp.pWinHttpSendRequest = (PWIN_HTTP_SEND_REQUEST)GetProcAddress(DllWinHttp.hDll, "WinHttpSendRequest");

    return TRUE;
}

/**
 A structure containing pointers to wininet.dll functions that can be used if
 they are found but programs do not have a hard dependency on.
 */
YORI_WININET_FUNCTIONS DllWinInet;

/**
 Load pointers to all optional WinInet.dll functions.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriLibLoadWinInetFunctions(VOID)
{

    if (DllWinInet.hDll != NULL) {
        return TRUE;
    }

    DllWinInet.hDll = YoriLibLoadLibraryFromSystemDirectory(_T("WININET.DLL"));
    if (DllWinInet.hDll == NULL) {
        return FALSE;
    }

    DllWinInet.pHttpQueryInfoA = (PHTTP_QUERY_INFOA)GetProcAddress(DllWinInet.hDll, "HttpQueryInfoA");
    DllWinInet.pHttpQueryInfoW = (PHTTP_QUERY_INFOW)GetProcAddress(DllWinInet.hDll, "HttpQueryInfoW");
    DllWinInet.pInternetCloseHandle = (PINTERNET_CLOSE_HANDLE)GetProcAddress(DllWinInet.hDll, "InternetCloseHandle");
    DllWinInet.pInternetOpenA = (PINTERNET_OPENA)GetProcAddress(DllWinInet.hDll, "InternetOpenA");
    DllWinInet.pInternetOpenW = (PINTERNET_OPENW)GetProcAddress(DllWinInet.hDll, "InternetOpenW");
    DllWinInet.pInternetOpenUrlA = (PINTERNET_OPEN_URLA)GetProcAddress(DllWinInet.hDll, "InternetOpenUrlA");
    DllWinInet.pInternetOpenUrlW = (PINTERNET_OPEN_URLW)GetProcAddress(DllWinInet.hDll, "InternetOpenUrlW");
    DllWinInet.pInternetReadFile = (PINTERNET_READ_FILE)GetProcAddress(DllWinInet.hDll, "InternetReadFile");

    return TRUE;
}

// vim:sw=4:ts=4:et:


<|start_filename|>builtins/history.c<|end_filename|>
/**
 * @file builtins/history.c
 *
 * Yori shell history output
 *
 * Copyright (c) 2018-2019 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>
#include <yoricall.h>
#include <yoriwin.h>

/**
 Help text to display to the user.
 */
const
CHAR strHistoryHelpText[] =
        "\n"
        "Displays or modifies recent command history.\n"
        "\n"
        "HISTORY [-license] [-c|-l <file>|-n lines]\n"
        "\n"
        "   -c             Clear current history\n"
        "   -l             Load history from a file\n"
        "   -n             The number of lines of history to output\n"
        "   -u             Display a menu for the user to select a command\n";

/**
 Display usage text to the user.
 */
BOOL
HistoryHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("History %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strHistoryHelpText);
    return TRUE;
}

/**
 A callback invoked when the ok button is clicked.

 @param Ctrl Pointer to the button that was clicked.
 */
VOID
HistoryOkButtonClicked(
    __in PYORI_WIN_CTRL_HANDLE Ctrl
    )
{
    PYORI_WIN_CTRL_HANDLE Parent;
    Parent = YoriWinGetControlParent(Ctrl);
    YoriWinCloseWindow(Parent, TRUE);
}

/**
 A callback invoked when the cancel button is clicked.

 @param Ctrl Pointer to the button that was clicked.
 */
VOID
HistoryCancelButtonClicked(
    __in PYORI_WIN_CTRL_HANDLE Ctrl
    )
{
    PYORI_WIN_CTRL_HANDLE Parent;
    Parent = YoriWinGetControlParent(Ctrl);
    YoriWinCloseWindow(Parent, FALSE);
}

/**
 Display a popup window containing a list of items.

 @param MenuOptions Pointer to an array of list items.

 @param NumberOptions The number of elements within the array.

 @param ActiveOption On successful completion, updated to contain the option
        that the user selected.

 @return TRUE to indicate that the user successfully selected an option, FALSE
         to indicate the menu could not be displayed or the user cancelled the
         operation.
 */
__success(return)
BOOL
HistoryCreateSynchronousMenu(
    __in PYORI_STRING MenuOptions,
    __in DWORD NumberOptions,
    __out PDWORD ActiveOption
    )
{
    PYORI_WIN_WINDOW_MANAGER_HANDLE WinMgr;
    PYORI_WIN_CTRL_HANDLE List;
    PYORI_WIN_WINDOW_HANDLE Parent;
    SMALL_RECT ListRect;
    COORD WindowSize;
    YORI_STRING Title;
    SMALL_RECT ButtonArea;
    YORI_STRING Caption;
    WORD ButtonWidth;
    PYORI_WIN_CTRL_HANDLE Ctrl;
    DWORD_PTR Result;

    if (NumberOptions == 0) {
        return FALSE;
    }

    if (!YoriWinOpenWindowManager(FALSE, &WinMgr)) {
        return FALSE;
    }

    if (!YoriWinGetWinMgrDimensions(WinMgr, &WindowSize)) {
        WindowSize.X = 40;
        WindowSize.X = 15;
    } else {
        WindowSize.X = (SHORT)(WindowSize.X * 4 / 5);
        if (WindowSize.X < 40) {
            WindowSize.X = 40;
        }

        WindowSize.Y = (SHORT)(WindowSize.Y * 3 / 4);
        if (WindowSize.Y < 12) {
            WindowSize.Y = 12;
        }
    }

    YoriLibConstantString(&Title, _T("History"));

    if (!YoriWinCreateWindow(WinMgr, WindowSize.X, WindowSize.Y, WindowSize.X, WindowSize.Y, YORI_WIN_WINDOW_STYLE_BORDER_SINGLE | YORI_WIN_WINDOW_STYLE_SHADOW_SOLID, &Title, &Parent)) {
        YoriWinCloseWindowManager(WinMgr);
        return FALSE;
    }

    YoriWinGetClientSize(Parent, &WindowSize);

    ListRect.Left = 1;
    ListRect.Top = 1;
    ListRect.Right = (SHORT)(WindowSize.X - 2);
    ListRect.Bottom = (SHORT)(WindowSize.Y - 3 - 1);

    List = YoriWinListCreate(Parent, &ListRect, YORI_WIN_LIST_STYLE_VSCROLLBAR);
    if (List == NULL) {
        YoriWinDestroyWindow(Parent);
        YoriWinCloseWindowManager(WinMgr);
        return FALSE;
    }

    if (!YoriWinListAddItems(List, MenuOptions, NumberOptions)) {
        YoriWinDestroyWindow(Parent);
        YoriWinCloseWindowManager(WinMgr);
        return FALSE;
    }

    YoriWinListSetActiveOption(List, NumberOptions - 1);

    ButtonWidth = (WORD)(sizeof("Cancel") - 1 + 2);

    ButtonArea.Top = (SHORT)(WindowSize.Y - 3);
    ButtonArea.Bottom = (SHORT)(ButtonArea.Top + 2);

    YoriLibConstantString(&Caption, _T("&Ok"));

    //
    //  WindowSize corresponds to dimensions, so rightmost cell is one
    //  less.  The button starts two buttons over, and each button
    //  has its client plus border chars, and there's an extra char
    //  between the buttons.
    //

    ButtonArea.Left = (SHORT)(WindowSize.X - 1 - 2 * (ButtonWidth + 2) - 1);
    ButtonArea.Right = (WORD)(ButtonArea.Left + ButtonWidth + 1);

    Ctrl = YoriWinButtonCreate(Parent, &ButtonArea, &Caption, YORI_WIN_BUTTON_STYLE_DEFAULT, HistoryOkButtonClicked);
    if (Ctrl == NULL) {
        YoriWinDestroyWindow(Parent);
        YoriWinCloseWindowManager(WinMgr);
        return FALSE;
    }

    YoriLibConstantString(&Caption, _T("&Cancel"));

    ButtonArea.Left = (SHORT)(WindowSize.X - 1 - (ButtonWidth + 2));
    ButtonArea.Right = (WORD)(ButtonArea.Left + ButtonWidth + 1);

    Ctrl = YoriWinButtonCreate(Parent, &ButtonArea, &Caption, YORI_WIN_BUTTON_STYLE_CANCEL, HistoryCancelButtonClicked);
    if (Ctrl == NULL) {
        YoriWinDestroyWindow(Parent);
        YoriWinCloseWindowManager(WinMgr);
        return FALSE;
    }

    Result = FALSE;
    if (!YoriWinProcessInputForWindow(Parent, &Result)) {
        Result = FALSE;
    }
    if (Result) {
        if (!YoriWinListGetActiveOption(List, ActiveOption)) {
            Result = FALSE;
        }
    }

    YoriWinDestroyWindow(Parent);
    YoriWinCloseWindowManager(WinMgr);
    return (BOOL)Result;
}


/**
 Load history from a file.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
HistoryLoadHistoryFromFile(
    __in PYORI_STRING FilePath
    )
{
    HANDLE FileHandle;
    PVOID LineContext = NULL;
    YORI_STRING LineString;

    FileHandle = CreateFile(FilePath->StartOfString,
                            GENERIC_READ,
                            FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                            NULL,
                            OPEN_EXISTING,
                            FILE_ATTRIBUTE_NORMAL,
                            NULL);

    if (FileHandle == NULL || FileHandle == INVALID_HANDLE_VALUE) {
        DWORD LastError = GetLastError();
        if (LastError != ERROR_FILE_NOT_FOUND) {
            LPTSTR ErrText = YoriLibGetWinErrorText(LastError);
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("history: open of %y failed: %s"), &FilePath, ErrText);
            YoriLibFreeWinErrorText(ErrText);
        }
        return FALSE;
    }

    YoriCallClearHistoryStrings();
    YoriLibInitEmptyString(&LineString);

    while (TRUE) {

        if (!YoriLibReadLineToString(&LineString, &LineContext, FileHandle)) {
            break;
        }

        //
        //  If we fail to add to history, stop.
        //

        if (!YoriCallAddHistoryString(&LineString)) {
            break;
        }
    }

    YoriLibLineReadCloseOrCache(LineContext);
    YoriLibFreeStringContents(&LineString);
    CloseHandle(FileHandle);
    return TRUE;
}

/**
 The set of operations this module is capable of performing.
 */
typedef enum _HISTORY_OP {
    HistoryDisplayHistory = 1,
    HistoryLoadHistory = 2,
    HistoryClearHistory = 3,
    HistoryDisplayUi = 4
} HISTORY_OP;

/**
 Display yori shell command history.

 @param ArgC The number of arguments.

 @param ArgV The argument array.

 @return ExitCode, zero for success, nonzero for failure.
 */
DWORD
YORI_BUILTIN_FN
YoriCmd_HISTORY(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    DWORD LineCount = 0;
    YORI_STRING Arg;
    YORI_STRING HistoryStrings;
    PYORI_STRING SourceFile = NULL;
    LPTSTR ThisVar;
    DWORD VarLen;
    HISTORY_OP Op;

    Op = HistoryDisplayHistory;
    YoriLibLoadNtDllFunctions();
    YoriLibLoadKernel32Functions();

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                HistoryHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2018"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("c")) == 0) {
                Op = HistoryClearHistory;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("l")) == 0) {
                if (ArgC > i + 1) {
                    Op = HistoryLoadHistory;
                    SourceFile = &ArgV[i + 1];
                    ArgumentUnderstood = TRUE;
                    i++;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("n")) == 0) {
                if (ArgC > i + 1) {
                    DWORD CharsConsumed;
                    LONGLONG llTemp;
                    if (YoriLibStringToNumber(&ArgV[i + 1], TRUE, &llTemp, &CharsConsumed) &&
                        CharsConsumed > 0) {

                        LineCount = (DWORD)llTemp;
                        ArgumentUnderstood = TRUE;
                        i++;
                    }
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("u")) == 0) {

                Op = HistoryDisplayUi;
                ArgumentUnderstood = TRUE;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    if (Op == HistoryClearHistory) {
        if (!YoriCallClearHistoryStrings()) {
            return EXIT_FAILURE;
        }
    } else if (Op == HistoryLoadHistory) {
        YORI_STRING FileName;
        if (!YoriLibUserStringToSingleFilePath(SourceFile, TRUE, &FileName)) {
            DWORD LastError = GetLastError();
            LPTSTR ErrText = YoriLibGetWinErrorText(LastError);
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("history: getfullpathname of %y failed: %s"), &ArgV[StartArg], ErrText);
            YoriLibFreeWinErrorText(ErrText);
            return EXIT_FAILURE;
        }
        if (!YoriCallSetUnloadRoutine(YoriLibLineReadCleanupCache)) {
            YoriLibFreeStringContents(&FileName);
            return EXIT_FAILURE;
        }
        if (!HistoryLoadHistoryFromFile(&FileName)) {
            YoriLibFreeStringContents(&FileName);
            return EXIT_FAILURE;
        }
        YoriLibFreeStringContents(&FileName);
    } else if (Op == HistoryDisplayHistory) {
        if (YoriCallGetHistoryStrings(LineCount, &HistoryStrings)) {
            ThisVar = HistoryStrings.StartOfString;
            while (*ThisVar != '\0') {
                VarLen = _tcslen(ThisVar);
                YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%s\n"), ThisVar);
                ThisVar += VarLen;
                ThisVar++;
            }
            YoriCallFreeYoriString(&HistoryStrings);
        }
    } else if (Op == HistoryDisplayUi) {
        if (YoriCallGetHistoryStrings(LineCount, &HistoryStrings)) {
            PYORI_STRING StringArray;
            DWORD StringCount;
            DWORD Index;
            StringCount = 0;
            ThisVar = HistoryStrings.StartOfString;
            while (*ThisVar != '\0') {
                VarLen = _tcslen(ThisVar);
                StringCount++;
                ThisVar += VarLen;
                ThisVar++;
            }

            StringArray = YoriLibMalloc(sizeof(YORI_STRING)*StringCount);
            if (StringArray == NULL) {
                YoriCallFreeYoriString(&HistoryStrings);
                return EXIT_FAILURE;
            }

            Index = 0;
            ThisVar = HistoryStrings.StartOfString;
            while (*ThisVar != '\0') {
                VarLen = _tcslen(ThisVar);
                YoriLibInitEmptyString(&StringArray[Index]);
                StringArray[Index].StartOfString = ThisVar;
                StringArray[Index].LengthInChars = VarLen;
                StringArray[Index].LengthAllocated = VarLen + 1;
                Index++;
                ThisVar += VarLen;
                ThisVar++;
            }

            if (!HistoryCreateSynchronousMenu(StringArray, StringCount, &Index)) {
                YoriLibFree(StringArray);
                YoriCallFreeYoriString(&HistoryStrings);
                return EXIT_FAILURE;
            }

            //
            //  If the shell supports it, populate the next command with the
            //  string for the user to edit.  If not, just execute it
            //  verbatim.
            //

            if (!YoriCallSetNextCommand(&StringArray[Index])) {
                YoriCallExecuteExpression(&StringArray[Index]);
            }
            YoriLibFree(StringArray);
            YoriCallFreeYoriString(&HistoryStrings);
        }
    }
    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>ypm/download.c<|end_filename|>
/**
 * @file ypm/download.c
 *
 * Yori shell package manager download packages for later/offline installation
 *
 * Copyright (c) 2018-2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>
#include <yoripkg.h>
#include "ypm.h"

/**
 Help text to display to the user.
 */
const
CHAR strYpmDownloadHelpText[] =
        "\n"
        "Download packages for later or offline installation.\n"
        "\n"
        "YPM [-license]\n"
        "YPM -download <source> <target>\n"
        "\n"
        "   <source>        Specifies a URL root to download from\n"
        "   <target>        Specifies a directory to download to\n";

/**
 Help text to display to the user.
 */
const
CHAR strYpmDownloadDailyHelpText[] =
        "\n"
        "Download latest daily packages for later or offline installation.\n"
        "\n"
        "YPM [-license]\n"
        "YPM -download-daily <target>\n"
        "\n"
        "   <target>        Specifies a directory to download to\n";

/**
 Help text to display to the user.
 */
const
CHAR strYpmDownloadStableHelpText[] =
        "\n"
        "Download latest stable packages for later or offline installation.\n"
        "\n"
        "YPM [-license]\n"
        "YPM -download-stable <target>\n"
        "\n"
        "   <target>        Specifies a directory to download to\n";

/**
 Display usage text to the user.
 */
BOOL
YpmDownloadHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Ypm %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strYpmDownloadHelpText);
    return TRUE;
}

/**
 Display usage text to the user.
 */
BOOL
YpmDownloadDailyHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Ypm %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strYpmDownloadDailyHelpText);
    return TRUE;
}

/**
 Display usage text to the user.
 */
BOOL
YpmDownloadStableHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Ypm %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strYpmDownloadStableHelpText);
    return TRUE;
}

/**
 Download packages for later or offline installation.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process.
 */
DWORD
YpmDownload(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    YORI_STRING Arg;
    PYORI_STRING SourcePath = NULL;
    PYORI_STRING FilePath = NULL;

    if (ArgC < 3) {
        YpmDownloadHelp();
        return EXIT_FAILURE;
    }

    SourcePath = &ArgV[1];
    FilePath = &ArgV[2];

    for (i = 3; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                YpmDownloadHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2017-2021"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                ArgumentUnderstood = TRUE;
                StartArg = i + 1;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    YoriPkgDownloadRemotePackages(SourcePath, FilePath);

    return EXIT_SUCCESS;
}

/**
 Download the latest daily packages for later or offline installation.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process.
 */
DWORD
YpmDownloadDaily(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    YORI_STRING Arg;
    YORI_STRING SourcePath;
    PYORI_STRING FilePath = NULL;

    if (ArgC < 2) {
        YpmDownloadDailyHelp();
        return EXIT_FAILURE;
    }

    FilePath = &ArgV[1];
    YoriLibConstantString(&SourcePath, _T("http://www.malsmith.net/download/?obj=yori/latest-daily/"));


    for (i = 2; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                YpmDownloadDailyHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2017-2021"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                ArgumentUnderstood = TRUE;
                StartArg = i + 1;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    YoriPkgDownloadRemotePackages(&SourcePath, FilePath);

    return EXIT_SUCCESS;
}

/**
 Download the latest stable packages for later or offline installation.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process.
 */
DWORD
YpmDownloadStable(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    YORI_STRING Arg;
    YORI_STRING SourcePath;
    PYORI_STRING FilePath = NULL;

    if (ArgC < 2) {
        YpmDownloadStableHelp();
        return EXIT_FAILURE;
    }

    FilePath = &ArgV[1];
    YoriLibConstantString(&SourcePath, _T("http://www.malsmith.net/download/?obj=yori/latest-stable/"));

    for (i = 2; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                YpmDownloadStableHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2017-2021"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                ArgumentUnderstood = TRUE;
                StartArg = i + 1;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    YoriPkgDownloadRemotePackages(&SourcePath, FilePath);

    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>attrib/attrib.c<|end_filename|>
/**
 * @file attrib/attrib.c
 *
 * Yori shell display and manipulate file attributes
 *
 * Copyright (c) 2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 Help text to display to the user.
 */
const
CHAR strAttribHelpText[] =
        "\n"
        "Display or manipulate file attributes.\n"
        "\n"
        "ATTRIB [/license] [+Attrs] [-Attrs] [/b] [/d] [/s] [/v] [<file>...]\n"
        "\n"
        "   /b             Use basic search criteria for files only\n"
        "   /d             Include directories as well as files\n"
        "   /s             Process files from all subdirectories\n"
        "   /v             Verbose output\n"
        "\n";


/**
 A structure to map a 32 bit flag value to a character to input or output
 when describing the flag to humans.  The character is expected to be
 unique to allow input by character to function.
 */
typedef struct _ATTRIB_CHAR_TO_DWORD_FLAG {

    /**
     The flag in native representation.
     */
    DWORD Flag;

    /**
     The character to display to the user.
     */
    TCHAR DisplayLetter;

    /**
     Unused in order to ensure structure alignment.
     */
    WORD  AlignmentPadding;

    /**
     Help text for the flag.
     */
    LPCSTR HelpText;

} ATTRIB_CHAR_TO_DWORD_FLAG, *PATTRIB_CHAR_TO_DWORD_FLAG;

/**
 A table that maps file attribute flags as returned by the system to character
 representations used in UI or specified by the user.  The order in this table
 corresponds to the order that flags will be displayed in on query.
 */
const ATTRIB_CHAR_TO_DWORD_FLAG
AttribFileAttrPairs[] = {
    {FILE_ATTRIBUTE_ARCHIVE,             'A', 0, "Archive file attribute"},
    {FILE_ATTRIBUTE_SYSTEM,              'S', 0, "System file attribute"},
    {FILE_ATTRIBUTE_HIDDEN,              'H', 0, "Hidden file attribute"},
    {FILE_ATTRIBUTE_READONLY,            'R', 0, "Read-only file attribute"},
    {FILE_ATTRIBUTE_OFFLINE,             'O', 0, "Offline file attribute"},
    {FILE_ATTRIBUTE_NOT_CONTENT_INDEXED, 'I', 0, "Not content indexed file attribute"},
    {FILE_ATTRIBUTE_NO_SCRUB_DATA,       'X', 0, "No scrub file attribute"},
    {FILE_ATTRIBUTE_INTEGRITY_STREAM,    'V', 0, "Integrity attribute"},
    {FILE_ATTRIBUTE_PINNED,              'P', 0, "Pinned attribute"},
    {FILE_ATTRIBUTE_UNPINNED,            'U', 0, "Unpinned attribute"},
    {FILE_ATTRIBUTE_STRICTLY_SEQUENTIAL, 'B', 0, "SMR Blob attribute"},
    };


/**
 Display usage text to the user.
 */
BOOL
AttribHelp(VOID)
{
    DWORD Index;
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Attrib %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strAttribHelpText);

    for (Index = 0; Index < sizeof(AttribFileAttrPairs)/sizeof(AttribFileAttrPairs[0]); Index++) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("   %c              %hs\n"), AttribFileAttrPairs[Index].DisplayLetter, AttribFileAttrPairs[Index].HelpText);
    }

    return TRUE;
}

/**
 Context passed for each file found.
 */
typedef struct _ATTRIB_CONTEXT {

    /**
     TRUE if enumeration is recursive, FALSE if it is within one directory
     only.
     */
    BOOLEAN Recursive;

    /**
     TRUE if directories should be included on wildcard matches.  FALSE if
     only files should be included.
     */
    BOOLEAN IncludeDirectories;

    /**
     TRUE if output should be generated for each file processed.  FALSE for
     silent processing.
     */
    BOOLEAN Verbose;

    /**
     The first error encountered when enumerating objects from a single arg.
     This is used to preserve file not found/path not found errors so that
     when the program falls back to interpreting the argument as a literal,
     if that still doesn't work, this is the error code that is displayed.
     */
    DWORD SavedErrorThisArg;

    /**
     The set of attribute flags to set on each matching file.
     */
    DWORD AttributesToSet;

    /**
     The set of attribute flags to clear on each matching file.
     */
    DWORD AttributesToClear;

    /**
     A string to record the unescaped path to display for files.  This is
     kept here so the allocation can be reused for later files.
     */
    YORI_STRING UnescapedPath;

    /**
     Records the total number of files processed within a single command line
     argument.
     */
    DWORDLONG FilesFoundThisArg;

    /**
     Records the total number of files processed.
     */
    DWORDLONG FilesFound;

} ATTRIB_CONTEXT, *PATTRIB_CONTEXT;

/**
 A callback that is invoked when a file is found that matches a search criteria
 specified in the set of strings to enumerate.

 @param FilePath Pointer to the file path that was found.

 @param FileInfo Information about the file.

 @param Depth Specifies recursion depth.  Ignored in this application.

 @param Context Pointer to the attrib context structure indicating the action
        to perform and populated with the file found.

 @return TRUE to continute enumerating, FALSE to abort.
 */
BOOL
AttribFileFoundCallback(
    __in PYORI_STRING FilePath,
    __in_opt PWIN32_FIND_DATA FileInfo,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    PATTRIB_CONTEXT AttribContext = (PATTRIB_CONTEXT)Context;
    DWORD LastError;
    LPTSTR ErrText;
    DWORD ExistingAttributes;
    DWORD NewAttributes;

    UNREFERENCED_PARAMETER(Depth);
    UNREFERENCED_PARAMETER(FileInfo);

    ASSERT(YoriLibIsStringNullTerminated(FilePath));

    AttribContext->FilesFoundThisArg++;

    ExistingAttributes = GetFileAttributes(FilePath->StartOfString);
    if (ExistingAttributes == (DWORD)-1) {
        LastError = GetLastError();
        ErrText = YoriLibGetWinErrorText(LastError);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("attrib: query of attributes failed: %y %s\n"), FilePath, ErrText);
        YoriLibFreeWinErrorText(ErrText);
        return TRUE;
    }

    if (AttribContext->AttributesToSet == 0 &&
        AttribContext->AttributesToClear == 0) {

        TCHAR StrAtts[sizeof(AttribFileAttrPairs)/sizeof(AttribFileAttrPairs[0]) + 1];
        DWORD Index;

        for (Index = 0; Index < sizeof(AttribFileAttrPairs)/sizeof(AttribFileAttrPairs[0]); Index++) {
            if (ExistingAttributes & AttribFileAttrPairs[Index].Flag) {
                StrAtts[Index] = AttribFileAttrPairs[Index].DisplayLetter;
            } else {
                StrAtts[Index] = ' ';
            }
        }

        StrAtts[Index] = '\0';

        if (!YoriLibUnescapePath(FilePath, &AttribContext->UnescapedPath)) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%s %y\n"), StrAtts, FilePath);
        } else {
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%s %y\n"), StrAtts, &AttribContext->UnescapedPath);
        }
    } else {

        NewAttributes = ExistingAttributes & ~(AttribContext->AttributesToClear);
        NewAttributes = NewAttributes | AttribContext->AttributesToSet;

        if (NewAttributes != ExistingAttributes) {
            if (!SetFileAttributes(FilePath->StartOfString, NewAttributes)) {
                LastError = GetLastError();
                ErrText = YoriLibGetWinErrorText(LastError);
                YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("attrib: modification of attributes failed: %y %s\n"), FilePath, ErrText);
                YoriLibFreeWinErrorText(ErrText);
                return TRUE;
            } else if (AttribContext->Verbose) {
                if (!YoriLibUnescapePath(FilePath, &AttribContext->UnescapedPath)) {
                    YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Updating %y\n"), FilePath);
                } else {
                    YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Updating %y\n"), &AttribContext->UnescapedPath);
                }
            }
        }
    }

    AttribContext->FilesFound++;

    return TRUE;
}

/**
 A callback that is invoked when a directory cannot be successfully enumerated.

 @param FilePath Pointer to the file path that could not be enumerated.

 @param ErrorCode The Win32 error code describing the failure.

 @param Depth Recursion depth, ignored in this application.

 @param Context Pointer to the context block indicating whether the
        enumeration was recursive.  Recursive enumerates do not complain
        if a matching file is not in every single directory, because
        common usage expects files to be in a subset of directories only.

 @return TRUE to continute enumerating, FALSE to abort.
 */
BOOL
AttribFileEnumerateErrorCallback(
    __in PYORI_STRING FilePath,
    __in DWORD ErrorCode,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    YORI_STRING UnescapedFilePath;
    BOOL Result = FALSE;
    PATTRIB_CONTEXT AttribContext = (PATTRIB_CONTEXT)Context;

    UNREFERENCED_PARAMETER(Depth);

    if (!YoriLibUnescapePath(FilePath, &AttribContext->UnescapedPath)) {
        UnescapedFilePath.StartOfString = FilePath->StartOfString;
        UnescapedFilePath.LengthInChars = FilePath->LengthInChars;
    } else {
        UnescapedFilePath.StartOfString = AttribContext->UnescapedPath.StartOfString;
        UnescapedFilePath.LengthInChars = AttribContext->UnescapedPath.LengthInChars;
    }

    if (ErrorCode == ERROR_FILE_NOT_FOUND || ErrorCode == ERROR_PATH_NOT_FOUND) {
        AttribContext->SavedErrorThisArg = ErrorCode;
        Result = TRUE;
    } else {
        LPTSTR ErrText = YoriLibGetWinErrorText(ErrorCode);
        YORI_STRING DirName;
        LPTSTR FilePart;
        YoriLibInitEmptyString(&DirName);
        DirName.StartOfString = UnescapedFilePath.StartOfString;
        FilePart = YoriLibFindRightMostCharacter(&UnescapedFilePath, '\\');
        if (FilePart != NULL) {
            DirName.LengthInChars = (DWORD)(FilePart - DirName.StartOfString);
        } else {
            DirName.LengthInChars = UnescapedFilePath.LengthInChars;
        }
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Enumerate of %y failed: %s"), &DirName, ErrText);
        YoriLibFreeWinErrorText(ErrText);
    }
    return Result;
}


/**
 Translate a string of characters into their corresponding attribute flags and
 return the result.

 @param String Pointer to a string containing one or more attribute characters.

 @return The result in attribute flags, or zero to indicate that at least one
         character could not be resolved to a flag.
 */
DWORD
AttribStringToFlags(
    __in PYORI_STRING String
    )
{
    DWORD Flags;
    DWORD Index;
    DWORD FlagIndex;
    TCHAR Char;

    Flags = 0;
    for (Index = 0; Index < String->LengthInChars; Index++) {
        Char = YoriLibUpcaseChar(String->StartOfString[Index]);

        for (FlagIndex = 0; FlagIndex < sizeof(AttribFileAttrPairs)/sizeof(AttribFileAttrPairs[0]); FlagIndex++) {
            if (Char == AttribFileAttrPairs[FlagIndex].DisplayLetter) {
                Flags = Flags | AttribFileAttrPairs[FlagIndex].Flag;
                break;
            }
        }

        if (FlagIndex == sizeof(AttribFileAttrPairs)/sizeof(AttribFileAttrPairs[0])) {
            return 0;
        }
    }

    return Flags;
}


#ifdef YORI_BUILTIN
/**
 The main entrypoint for the attrib builtin command.
 */
#define ENTRYPOINT YoriCmd_YATTRIB
#else
/**
 The main entrypoint for the attrib standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the attrib cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process, zero on success, nonzero on failure.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOLEAN ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    DWORD MatchFlags;
    DWORD Result;
    DWORD NewAttributes;
    BOOLEAN BasicEnumeration = FALSE;
    BOOLEAN MatchAllFiles = FALSE;
    ATTRIB_CONTEXT AttribContext;
    TCHAR PrefixChar;
    YORI_STRING Arg;
    YORI_STRING FullPath;

    ZeroMemory(&AttribContext, sizeof(AttribContext));
    YoriLibInitEmptyString(&Arg);

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;

        Arg.StartOfString = ArgV[i].StartOfString;
        Arg.LengthInChars = ArgV[i].LengthInChars;

        PrefixChar = '\0';
        if (Arg.LengthInChars > 0) {
            if (Arg.StartOfString[0] == '/' ||
                Arg.StartOfString[0] == '-' ||
                Arg.StartOfString[0] == '+') {

                PrefixChar = Arg.StartOfString[0];
                Arg.StartOfString++;
                Arg.LengthInChars--;
            }
        }

        //
        //  Unlike everything else, options here use / exclusively because
        //  - is for removing attributes.
        //

        if (PrefixChar == '/') {
            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                AttribHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2017-2021"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("b")) == 0) {
                BasicEnumeration = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("d")) == 0) {
                AttribContext.IncludeDirectories = TRUE;
                ArgumentUnderstood = TRUE;

            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("s")) == 0) {
                AttribContext.Recursive = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("v")) == 0) {
                AttribContext.Verbose = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                ArgumentUnderstood = TRUE;
                StartArg = i + 1;
                break;
            }
        } else if (PrefixChar == '-') {
            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                AttribHelp();
                return EXIT_SUCCESS;
            } else {
                NewAttributes = AttribStringToFlags(&Arg);
                if (NewAttributes != 0) {
                    AttribContext.AttributesToClear |= NewAttributes;
                    ArgumentUnderstood = TRUE;
                }
            }
        } else if (PrefixChar == '+') {
            NewAttributes = AttribStringToFlags(&Arg);
            if (NewAttributes != 0) {
                AttribContext.AttributesToSet |= NewAttributes;
                ArgumentUnderstood = TRUE;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    if (StartArg == 0 || StartArg == ArgC) {
        MatchAllFiles = TRUE;
    }

#if YORI_BUILTIN
    YoriLibCancelEnable();
#endif

    Result = EXIT_SUCCESS;
    MatchFlags = YORILIB_FILEENUM_RETURN_FILES;
    if (AttribContext.IncludeDirectories) {
        MatchFlags |= YORILIB_FILEENUM_RETURN_DIRECTORIES;
    }
    if (AttribContext.Recursive) {
        MatchFlags |= YORILIB_FILEENUM_RECURSE_BEFORE_RETURN;
        if (!AttribContext.IncludeDirectories) {
            MatchFlags |= YORILIB_FILEENUM_RECURSE_PRESERVE_WILD;
        }
    }
    if (BasicEnumeration) {
        MatchFlags |= YORILIB_FILEENUM_BASIC_EXPANSION;
    }

    if (MatchAllFiles) {
        YoriLibConstantString(&Arg, _T("*"));

        if (!YoriLibUserStringToSingleFilePath(&Arg, TRUE, &FullPath)) {
            return EXIT_FAILURE;
        }

        AttribContext.FilesFoundThisArg = 0;
        AttribContext.SavedErrorThisArg = ERROR_SUCCESS;

        YoriLibForEachFile(&FullPath,
                           MatchFlags,
                           0,
                           AttribFileFoundCallback,
                           AttribFileEnumerateErrorCallback,
                           &AttribContext);

        YoriLibFreeStringContents(&FullPath);

        if (AttribContext.SavedErrorThisArg != ERROR_SUCCESS) {
            Result = EXIT_FAILURE;
        }

    } else {
        for (i = StartArg; i < ArgC; i++) {

            AttribContext.FilesFoundThisArg = 0;
            AttribContext.SavedErrorThisArg = ERROR_SUCCESS;

            YoriLibForEachFile(&ArgV[i],
                               MatchFlags,
                               0,
                               AttribFileFoundCallback,
                               AttribFileEnumerateErrorCallback,
                               &AttribContext);

            if (AttribContext.FilesFoundThisArg == 0) {
                YoriLibInitEmptyString(&FullPath);
                if (YoriLibUserStringToSingleFilePath(&ArgV[i], TRUE, &FullPath)) {
                    AttribFileFoundCallback(&FullPath, NULL, 0, &AttribContext);
                    YoriLibFreeStringContents(&FullPath);
                }

                if (AttribContext.SavedErrorThisArg != ERROR_SUCCESS) {
                    YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("File or directory not found: %y\n"), &ArgV[i]);
                }
            }

            if (AttribContext.SavedErrorThisArg != ERROR_SUCCESS) {
                Result = EXIT_FAILURE;
            }
        }
    }

    YoriLibFreeStringContents(&AttribContext.UnescapedPath);

    if (AttribContext.FilesFound == 0) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("attrib: no matching files found\n"));
        Result = EXIT_FAILURE;
    }

    return Result;
}

// vim:sw=4:ts=4:et:


<|start_filename|>libsh/Makefile<|end_filename|>

compile: yorish.lib

!INCLUDE "..\config\common.mk"

OBJS=\
	 builtin.obj  \
	 cmdbuf.obj   \
	 exec.obj     \
	 parse.obj    \

yorish.lib: $(OBJS)
	@echo $@
	@$(LIB32) $(LIBFLAGS) $(OBJS) /out:yorish.lib



<|start_filename|>config/ver.mk<|end_filename|>

!IFNDEF YORI_BASE_VER_MAJOR
YORI_BASE_VER_MAJOR=1
YORI_BASE_VER_MINOR=60

# Which previously shipped outside Yori with a version of 2.0.
# To maintain this legacy, it gets its own version.

WHICH_VER_MAJOR=2
WHICH_VER_MINOR=15

YORI_VER_MAJOR=$(YORI_BASE_VER_MAJOR)
YORI_VER_MINOR=$(YORI_BASE_VER_MINOR)
!ENDIF


<|start_filename|>lib/dyld_cab.c<|end_filename|>
/**
 * @file lib/dyld_cab.c
 *
 * Yori dynamically loaded cabinet function support with static fallback
 *
 * Copyright (c) 2018-2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "yoripch.h"
#include "yorilib.h"

#ifdef YORI_FDI_SUPPORT
/**
 Forward declaration of FDICreate when static linking.
 */
CAB_FDI_CREATE FDICreate;

/**
 Forward declaration of FDICreate when static linking.
 */
CAB_FDI_COPY FDICopy;

/**
 Forward declaration of FDIDestroy when static linking.
 */
CAB_FDI_DESTROY FDIDestroy;
#endif

/**
 A structure containing pointers to cabinet.dll functions that can be used if
 they are found but programs do not have a hard dependency on.
 */
YORI_CABINET_FUNCTIONS DllCabinet;

/**
 Load pointers to all optional cabinet.dll functions.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriLibLoadCabinetFunctions(VOID)
{
    if (DllCabinet.hDll != NULL) {
        return TRUE;
    }

    DllCabinet.hDll = YoriLibLoadLibraryFromSystemDirectory(_T("CABINET.DLL"));
    if (DllCabinet.hDll == NULL) {
#ifdef YORI_FDI_SUPPORT
        DllCabinet.pFdiCreate = FDICreate;
        DllCabinet.pFdiCopy = FDICopy;
        DllCabinet.pFdiDestroy = FDIDestroy;
#endif
        return FALSE;
    }

    DllCabinet.pFciAddFile = (PCAB_FCI_ADD_FILE)GetProcAddress(DllCabinet.hDll, "FCIAddFile");
    DllCabinet.pFciCreate = (PCAB_FCI_CREATE)GetProcAddress(DllCabinet.hDll, "FCICreate");
    DllCabinet.pFciDestroy = (PCAB_FCI_DESTROY)GetProcAddress(DllCabinet.hDll, "FCIDestroy");
    DllCabinet.pFciFlushCabinet = (PCAB_FCI_FLUSH_CABINET)GetProcAddress(DllCabinet.hDll, "FCIFlushCabinet");
    DllCabinet.pFciFlushFolder = (PCAB_FCI_FLUSH_FOLDER)GetProcAddress(DllCabinet.hDll, "FCIFlushFolder");
    DllCabinet.pFdiCreate = (PCAB_FDI_CREATE)GetProcAddress(DllCabinet.hDll, "FDICreate");
    DllCabinet.pFdiCopy = (PCAB_FDI_COPY)GetProcAddress(DllCabinet.hDll, "FDICopy");
    DllCabinet.pFdiDestroy = (PCAB_FDI_DESTROY)GetProcAddress(DllCabinet.hDll, "FDIDestroy");
    return TRUE;
}

// vim:sw=4:ts=4:et:


<|start_filename|>sh/yoristru.h<|end_filename|>
/**
 * @file sh/yoristru.h
 *
 * Yori shell structures header file
 *
 * Copyright (c) 2017-2019 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

/**
 Information about a previous command executed by the user.
 */
typedef struct _YORI_SH_HISTORY_ENTRY {

    /**
     The links for this history entry.
     */
    YORI_LIST_ENTRY ListEntry;

    /**
     The command that was executed by the user.
     */
    YORI_STRING CmdLine;
} YORI_SH_HISTORY_ENTRY, *PYORI_SH_HISTORY_ENTRY;

/**
 Information about a single tab complete match.
 */
typedef struct _YORI_SH_TAB_COMPLETE_MATCH {

    /**
     The list entry for this match.  Paired with @ref
     YORI_SH_TAB_COMPLETE_CONTEXT::MatchList .
     */
    YORI_LIST_ENTRY ListEntry;

    /**
     The hash entry for this match.  Paired with @ref
     YORI_SH_TAB_COMPLETE_CONTEXT::MatchHashTable .
     */
    YORI_HASH_ENTRY HashEntry;

    /**
     The string corresponding to this match.
     */
    YORI_STRING Value;

    /**
     The offset to place the cursor within the match.  If zero, the cursor
     is placed at the end of the string.  Note that it generally wouldn't
     make sense for tab complete (which is generating characters) to then
     want the cursor to be at the true beginning of the string.
     */
    DWORD CursorOffset;

} YORI_SH_TAB_COMPLETE_MATCH, *PYORI_SH_TAB_COMPLETE_MATCH;

/**
 A set of tab completion match types that can be performed.
 */
typedef enum _YORI_SH_TAB_COMPLETE_SEARCH_TYPE {
    YoriTabCompleteSearchExecutables = 1,
    YoriTabCompleteSearchFiles = 2,
    YoriTabCompleteSearchHistory = 3,
    YoriTabCompleteSearchArguments = 4
} YORI_SH_TAB_COMPLETE_SEARCH_TYPE;

/**
 Information about the state of tab completion.
 */
typedef struct _YORI_SH_TAB_COMPLETE_CONTEXT {

    /**
     Indicates the number of times tab has been repeatedly pressed.  This
     is reset if any other key is pressed instead of tab.  It is used to
     determine if the tab context requires initialization for the first
     tab, and where to resume from for later tabs.
     */
    DWORD TabCount;

    /**
     Indicates the number of characters of valid data exists in an argument
     when making suggestions.  Not used outside of suggestions.
     */
    DWORD CurrentArgLength;

    /**
     Indicates the TabFlags passed when building any match list.  This is
     used to detect a later, incompatible set of flags that implies the
     list should be reconstructed.
     */
    DWORD TabFlagsUsedCreatingList;

    /**
     Indicates which data source to search through.
     */
    YORI_SH_TAB_COMPLETE_SEARCH_TYPE SearchType;

    /**
     TRUE if later compares should be case sensitive.  This is used when
     refining suggestions.
     */
    BOOLEAN CaseSensitive;

    /**
     TRUE if when populating suggestions entries were skipped due to not
     being a full prefix match.  This implies a subsequent tab completion
     needs to recompute.
     */
    BOOLEAN PotentialNonPrefixMatch;

    /**
     A list of matches that apply to the criteria that was searched.
     */
    YORI_LIST_ENTRY MatchList;

    /**
     A hash table of matches that apply to the criteria that was searched.
     This is used to efficiently strip duplicates.
     */
    PYORI_HASH_TABLE MatchHashTable;

    /**
     Pointer to the previously returned match.  If the user repeatedly hits
     tab, we advance to the next match.
     */
    PYORI_SH_TAB_COMPLETE_MATCH PreviousMatch;

    /**
     The matching criteria that is being searched for.  This is typically
     the string that was present when the user first hit tab followed by
     a "*".
     */
    YORI_STRING SearchString;

    /**
     The offset in characters from the beginning of SearchString to where
     the cursor currently is.
     */
    DWORD SearchStringOffset;

} YORI_SH_TAB_COMPLETE_CONTEXT, *PYORI_SH_TAB_COMPLETE_CONTEXT;

/**
 The context of a line that is currently being entered by the user.
 */
typedef struct _YORI_SH_INPUT_BUFFER {

    /**
     Handle to standard input when it's a console.
     */
    HANDLE ConsoleInputHandle;

    /**
     Handle to standard output when it's a console.
     */
    HANDLE ConsoleOutputHandle;

    /**
     Pointer to a string containing the text as being entered by the user.
     */
    YORI_STRING String;

    /**
     The current offset within @ref String that the user is modifying.
     */
    DWORD CurrentOffset;

    /**
     The number of characters that were filled in prior to a key press
     being evaluated.
     */
    DWORD PreviousCharsDisplayed;

    /**
     The current position that was selected prior to a key press being
     evaluated.
     */
    DWORD PreviousCurrentOffset;

    /**
     The number of times the tab key had been pressed prior to a key being
     evaluated.
     */
    DWORD PriorTabCount;

    /**
     The first character in the buffer that may have changed since the last
     draw.
     */
    DWORD DirtyBeginOffset;

    /**
     The last character in the buffer that may have changed since the last
     draw.
     */
    DWORD DirtyLength;

    /**
     TRUE if the input should be in insert mode, FALSE if it should be
     overwrite mode.
     */
    BOOL InsertMode;

    /**
     Information about how to display the cursor.
     */
    CONSOLE_CURSOR_INFO CursorInfo;

    /**
     The size of the console buffer.  Used to detect and avoid processing
     bogus buffer resize notifications.
     */
    COORD ConsoleBufferDimensions;

    /**
     Pointer to the currently selected history entry when navigating through
     history.
     */
    PYORI_LIST_ENTRY HistoryEntryToUse;

    /**
     When inputting a character by value, the current value that has been
     accumulated (since this requires multiple key events.)
     */
    DWORD NumericKeyValue;

    /**
     Indicates how to interpret the NumericKeyValue.  Ascii uses CP_OEMCP,
     Ansi uses CP_ACP, Unicode is direct.  Also note that Unicode takes
     input in hexadecimal to match the normal U+xxxx specification.
     */
    YORI_LIB_NUMERIC_KEY_TYPE NumericKeyType;

    /**
     The last known bitmask of mouse button state.
     */
    DWORD PreviousMouseButtonState;

    /**
     The tick count when the window was last made active, or zero if a
     window activation has not been observed since this input line began.
     */
    DWORD WindowActivatedTick;

    /**
     The tick count when the selection was started, or zero if no selection
     has been started.
     */
    DWORD SelectionStartedTick;

    /**
     Description of the current selected region.
     */
    YORILIB_SELECTION Selection;

    /**
     Description of the current mouseover region.
     */
    YORILIB_SELECTION Mouseover;

    /**
     Extra information specific to tab completion processing.
     */
    YORI_SH_TAB_COMPLETE_CONTEXT TabContext;

    /**
     Set to TRUE if the suggestion string has changed and requires
     redisplay.
     */
    BOOLEAN SuggestionDirty;

    /**
     Set to TRUE if the suggestion candidates have been calculated and do not
     need to be recalculated.  Set to FALSE if a subsequent change to the
     buffer invalidates the previously calculated set.
     */
    BOOLEAN SuggestionPopulated;

    /**
     The currently active suggestion string.
     */
    YORI_STRING SuggestionString;

    /**
     If TRUE, the search buffer is the active buffer where keystrokes and
     backspace keys should be delivered to.  If FALSE, keystrokes are
     added to the input buffer.
     */
    BOOL SearchMode;

    /**
     The offset as it was when the search operation started.  This is used
     if no match is found or a search is cancelled.
     */
    DWORD PreSearchOffset;

    /**
     The current search string, when searching within the buffer itself.
     */
    YORI_STRING SearchString;

} YORI_SH_INPUT_BUFFER, *PYORI_SH_INPUT_BUFFER;

/**
 A structure defining a mapping between a command name and a function to
 execute.  This is used to populate builtin commands.
 */
typedef struct _YORI_SH_BUILTIN_NAME_MAPPING {

    /**
     The command name.
     */
    LPTSTR CommandName;

    /**
     Pointer to the function to execute.
     */
    PYORI_CMD_BUILTIN BuiltinFn;
} YORI_SH_BUILTIN_NAME_MAPPING, *PYORI_SH_BUILTIN_NAME_MAPPING;

/**
 A structure defining an initial mapping of alias to value.
 */
typedef struct _YORI_SH_DEFAULT_ALIAS_ENTRY {

    /**
     The initial alias name.
     */
    LPTSTR Alias;

    /**
     The initial value.
     */
    LPTSTR Value;
} YORI_SH_DEFAULT_ALIAS_ENTRY, *PYORI_SH_DEFAULT_ALIAS_ENTRY;

extern CONST YORI_SH_DEFAULT_ALIAS_ENTRY YoriShDefaultAliasEntries[];

/**
 A structure containing state that is global across the Yori shell process.
 */
typedef struct _YORI_SH_GLOBALS {

    /**
     The exit code ("error level") of the previous process to complete.
     */
    DWORD ErrorLevel;

    /**
     When ExitProcess is set to TRUE, this is set to the code that the shell
     should return as its exit code.
     */
    DWORD ExitProcessExitCode;

    /**
     The most recent Job ID that was assigned.
    */
    DWORD PreviousJobId;

    /**
     A pointer to the argument count of the currently active builtin
     command before escapes have been removed.  This can be given to a
     builtin if it needs to know the original escaped string.
     */
    DWORD EscapedArgC;

    /**
     A pointer to the argument array of the currently active builtin
     command before escapes have been removed.  This can be given to a
     builtin if it needs to know the original escaped string.
     */
    PYORI_STRING EscapedArgV;

    /**
     Count of recursion depth.  This is incremented when calling a builtin
     or when the shell is invoked from a subshell, and decremented when
     these return.  A recursion depth of zero implies a shell ready for user
     interaction.
     */
    DWORD RecursionDepth;

    /**
     Count of prompt recursion depth.  This is the number of characters to
     display when $+$ is used.
     */
    DWORD PromptRecursionDepth;

    /**
     The current revision number of the environment variables in the process.
     This is incremented whenever a change occurs to the environment which
     may imply that cached state about shell behavior needs to be reloaded.
     */
    DWORD EnvironmentGeneration;

    /**
     The number of ms to wait before suggesting the completion to a command.
     */
    DWORD DelayBeforeSuggesting;

    /**
     The minimum number of characters that the user must enter before
     suggestions occur.
     */
    DWORD MinimumCharsInArgBeforeSuggesting;

    /**
     The generation of the environment last time input parameters were
     refreshed.
     */
    DWORD InputParamsGeneration;

    /**
     A handle to a thread which is saving restart state.  Note that this may
     be NULL if no thread has been created or if it has completed.
     */
    HANDLE RestartSaveThread;

    /**
     List of command history.
     */
    YORI_LIST_ENTRY CommandHistory;

    /**
     The contents of the YORIPRECMD environment variable.
     */
    YORI_STRING PreCmdVariable;

    /**
     The generation of the environment at the time the variable was queried.
     */
    DWORD PreCmdGeneration;

    /**
     The contents of the YORIPOSTCMD environment variable.
     */
    YORI_STRING PostCmdVariable;

    /**
     The generation of the environment at the time the variable was queried.
     */
    DWORD PostCmdGeneration;

    /**
     The contents of the YORIPROMPT environment variable.
     */
    YORI_STRING PromptVariable;

    /**
     The generation of the environment at the time the variable was queried.
     */
    DWORD PromptGeneration;

    /**
     The contents of the YORITITLE environment variable.
     */
    YORI_STRING TitleVariable;

    /**
     The generation of the environment at the time the variable was queried.
     */
    DWORD TitleGeneration;

    /**
     The offset within NextCommand to initialize the cursor to.
     */
    DWORD NextCommandOffset;

    /**
     The text to use as a prepopulated string for the next user command.
     */
    YORI_STRING NextCommand;

    /**
     Two buffers that contain the current directory.  After each command, this
     state is loaded into the next buffer, allowing comparison with the
     previous current directory.  The ActiveCurrentDirectory index below
     indicates which buffer to use.
     */
    YORI_STRING CurrentDirectoryBuffers[2];

    /**
     Which of the two current directory buffers above contains the current
     directory.
     */
    DWORD ActiveCurrentDirectory;

    /**
     The current yanked string, similar to the system clipboard but used only
     for the kill and yank commands (Ctrl+K and Ctrl+Y).
     */
    YORI_STRING YankBuffer;

    /**
     When set to TRUE, the output device supports VT sequences.  When FALSE,
     VT sequences are only supported by YoriLib.
     */
    BOOLEAN OutputSupportsVt;

    /**
     When set to TRUE, the capabilities of the output device for VT sequences
     have been determined.  When FALSE, the state is not yet known.
     */
    BOOLEAN OutputSupportsVtDetermined;

    /**
     When set to TRUE, the process should end rather than seek another
     command.
     */
    BOOLEAN ExitProcess;

    /**
     When set to TRUE, indicates this process has been spawned as a subshell
     to execute builtin commands from a monolithic shell.
     */
    BOOLEAN SubShell;

    /**
     Set to TRUE once the process has initialized COM.
     */
    BOOLEAN InitializedCom;

    /**
     Set to TRUE if the process has set the taskbar button to any non-default
     state.
     */
    BOOLEAN TaskUiActive;

    /**
     Set to TRUE to indicate child processes launched without explicit user
     request, including to generate prompts and titles.  This implies ignoring
     any task UI state updates, and not processing input when waiting for
     process completion.
     */
    BOOLEAN ImplicitSynchronousTaskActive;

    /**
     Set to TRUE to disable the console's quickedit before inputting
     commands and enable it before executing them.
     */
    BOOLEAN YoriQuickEdit;

    /**
     Set to TRUE to include a trailing backslash when completing a directory
     as part of tab completion or suggestions.
     */
    BOOLEAN CompletionTrailingSlash;

    /**
     Set to TRUE to tell tab completion to list out all options when multiple
     are available instead of selecting the first.
     */
    BOOLEAN CompletionListAll;

    /**
     TRUE if mouseover support is enabled, FALSE if it is disabled.  Note this
     is currently enabled by default.
     */
    BOOLEAN MouseoverEnabled;

    /**
     TRUE if this is an interactive shell, meaning one spawned without /c or
     /ss.  In particular, note that /k is considered interactive.
     */
    BOOLEAN InteractiveMode;

    /**
     The Win32 color to use when changing text color due to a mouse over.
     */
    WORD MouseoverColor;

} YORI_SH_GLOBALS, *PYORI_SH_GLOBALS;

extern YORI_SH_GLOBALS YoriShGlobal;

// vim:sw=4:ts=4:et:


<|start_filename|>strcmp/strcmp.c<|end_filename|>
/**
 * @file strcmp/strcmp.c
 *
 * Yori shell compare strings
 *
 * Copyright (c) 2018-2019 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 Help text to display to the user.
 */
const
CHAR strStrCmpHelpText[] =
        "\n"
        "Compare two strings.\n"
        "\n"
        "STRCMP [-license] [-i] [--] <string><operator><string>\n"
        "\n"
        "   --             Treat all further arguments as comparison parameters\n"
        "   -i             Match case insensitively\n"
        "\n"
        "Operators are:\n"
        "   ==             Strings match exactly\n"
        "   !=             Strings do not match\n"
        "   *=             First string is contained in the second string\n";

/**
 Display usage text to the user.
 */
BOOL
StrCmpHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("StrCmp %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strStrCmpHelpText);
    return TRUE;
}

/**
 An array index for an operator indicating a match.
 */
#define STRCMP_OPERATOR_EXACT_MATCH 0

/**
 An array index for an operator indicating a mismatch.
 */
#define STRCMP_OPERATOR_NO_MATCH    1

/**
 An array index for an operator indicating a substring.
 */
#define STRCMP_OPERATOR_SUBSTRING   2

/**
 An array index beyond the array, ie., the number of elements in the array.
 */
#define STRCMP_OPERATOR_BEYOND_MAX  3

#ifdef YORI_BUILTIN
/**
 The main entrypoint for the strcmp builtin command.
 */
#define ENTRYPOINT YoriCmd_STRCMP
#else
/**
 The main entrypoint for the strcmp standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the strcmp cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process, zero for true and nonzero for false.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD StartArg = 0;
    DWORD i;
    YORI_STRING Expression;
    YORI_STRING Arg;
    YORI_STRING FirstPart;
    YORI_STRING SecondPart;
    YORI_STRING OperatorMatches[STRCMP_OPERATOR_BEYOND_MAX];
    PYORI_STRING MatchingOperator;
    PYORI_STRING FoundMatch;
    DWORD OperatorIndex;
    BOOL MatchInsensitive;
    DWORD Result;

    MatchInsensitive = FALSE;

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                StrCmpHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2018-2019"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("i")) == 0) {
                MatchInsensitive = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                if (i + 1 < ArgC) {
                    ArgumentUnderstood = TRUE;
                    StartArg = i + 1;
                    break;
                }
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    if (StartArg == 0) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("strcmp: missing argument\n"));
        return EXIT_FAILURE;
    }

    if (!YoriLibBuildCmdlineFromArgcArgv(ArgC - StartArg, &ArgV[StartArg], TRUE, FALSE, &Expression)) {
        return EXIT_FAILURE;
    }

    YoriLibConstantString(&OperatorMatches[STRCMP_OPERATOR_EXACT_MATCH], _T("=="));
    YoriLibConstantString(&OperatorMatches[STRCMP_OPERATOR_NO_MATCH], _T("!="));
    YoriLibConstantString(&OperatorMatches[STRCMP_OPERATOR_SUBSTRING], _T("*="));

    MatchingOperator = YoriLibFindFirstMatchingSubstring(&Expression, sizeof(OperatorMatches)/sizeof(OperatorMatches[0]), OperatorMatches, &OperatorIndex);
    if (MatchingOperator == NULL) {
        YoriLibFreeStringContents(&Expression);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("strcmp: missing operator\n"));
        return EXIT_FAILURE;
    }

    YoriLibInitEmptyString(&FirstPart);

    FirstPart.StartOfString = Expression.StartOfString;
    FirstPart.LengthInChars = OperatorIndex;

    YoriLibInitEmptyString(&SecondPart);

    SecondPart.StartOfString = Expression.StartOfString + OperatorIndex + MatchingOperator->LengthInChars;
    SecondPart.LengthInChars = Expression.LengthInChars - OperatorIndex - MatchingOperator->LengthInChars;

    Result = EXIT_FAILURE;

    if (MatchingOperator == &OperatorMatches[STRCMP_OPERATOR_EXACT_MATCH]) {

        if (MatchInsensitive) {
            if (YoriLibCompareStringInsensitive(&FirstPart, &SecondPart) == 0) {
                Result = EXIT_SUCCESS;
            } else {
                Result = EXIT_FAILURE;
            }
        } else {
            if (YoriLibCompareString(&FirstPart, &SecondPart) == 0) {
                Result = EXIT_SUCCESS;
            } else {
                Result = EXIT_FAILURE;
            }
        }

    } else if (MatchingOperator == &OperatorMatches[STRCMP_OPERATOR_NO_MATCH]) {

        if (MatchInsensitive) {
            if (YoriLibCompareStringInsensitive(&FirstPart, &SecondPart) == 0) {
                Result = EXIT_FAILURE;
            } else {
                Result = EXIT_SUCCESS;
            }
        } else {
            if (YoriLibCompareString(&FirstPart, &SecondPart) == 0) {
                Result = EXIT_FAILURE;
            } else {
                Result = EXIT_SUCCESS;
            }
        }

    } else if (MatchingOperator == &OperatorMatches[STRCMP_OPERATOR_SUBSTRING]) {

        if (MatchInsensitive) {
            FoundMatch = YoriLibFindFirstMatchingSubstringInsensitive(&SecondPart, 1, &FirstPart, NULL);
            if (FoundMatch != NULL) {
                Result = EXIT_SUCCESS;
            } else {
                Result = EXIT_FAILURE;
            }
        } else {
            FoundMatch = YoriLibFindFirstMatchingSubstring(&SecondPart, 1, &FirstPart, NULL);
            if (FoundMatch != NULL) {
                Result = EXIT_SUCCESS;
            } else {
                Result = EXIT_FAILURE;
            }
        }

    } else {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("strcmp: operator not implemented\n"));
        return EXIT_FAILURE;
    }

    YoriLibFreeStringContents(&Expression);

    return Result;
}

// vim:sw=4:ts=4:et:


<|start_filename|>dircase/dircase.c<|end_filename|>
/**
 * @file dircase/dircase.c
 *
 * Yori shell mark directories for case sensitive or insensitive semantics
 *
 * Copyright (c) 2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 Help text to display to the user.
 */
const
CHAR strDirCaseHelpText[] =
        "\n"
        "Mark directories for case sensitive or insensitive semantics.\n"
        "\n"
        "DIRCASE [-license] [-b] <-ci|-cs> [-s] [<directory>...]\n"
        "\n"
        "   -b             Use basic search criteria for directories only\n"
        "   -ci            Set the directories to case insensitive behavior\n"
        "   -cs            Set the directories to case sensitive behavior\n"
        "   -s             Process directories from all subdirectories\n"
        "   -v             Verbose output\n";

/**
 Display usage text to the user.
 */
BOOL
DirCaseHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("DirCase %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strDirCaseHelpText);
    return TRUE;
}

/**
 Context passed for each file found.
 */
typedef struct _DIRCASE_CONTEXT {

    /**
     TRUE if directories should be case sensitive.  FALSE if they should be
     case insensitive.
     */
    BOOLEAN CaseSensitive;

    /**
     TRUE if enumeration is recursive, FALSE if it is within one directory
     only.
     */
    BOOLEAN Recursive;

    /**
     TRUE if output should be generated for each file processed.  FALSE for
     silent processing.
     */
    BOOLEAN Verbose;

    /**
     Records the total number of directories found.
     */
    LONGLONG DirsFound;

    /**
     Records the total number of directories processed.
     */
    LONGLONG DirsModified;

} DIRCASE_CONTEXT, *PDIRCASE_CONTEXT;

/**
 A callback that is invoked when a directory is found that matches a search
 criteria specified in the set of strings to enumerate.

 @param FilePath Pointer to the file path that was found.

 @param FileInfo Information about the directory.

 @param Depth Specifies recursion depth.  Ignored in this application.

 @param Context Pointer to the dircase context structure indicating the action
        to perform and populated with the directory found.

 @return TRUE to continute enumerating, FALSE to abort.
 */
BOOL
DirCaseFileFoundCallback(
    __in PYORI_STRING FilePath,
    __in PWIN32_FIND_DATA FileInfo,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    PDIRCASE_CONTEXT DirCaseContext = (PDIRCASE_CONTEXT)Context;
    YORI_FILE_CASE_SENSITIVE_INFORMATION CaseSensitiveInfo;
    IO_STATUS_BLOCK IoStatusBlock;
    HANDLE hDir;
    DWORD AccessRequired;
    DWORD Status;

    UNREFERENCED_PARAMETER(FileInfo);
    UNREFERENCED_PARAMETER(Depth);

    ASSERT(YoriLibIsStringNullTerminated(FilePath));
    ASSERT((FileInfo->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0);

    DirCaseContext->DirsFound++;

    if (DirCaseContext->Verbose) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Updating %y...\n"), FilePath);
    }

    AccessRequired = FILE_WRITE_ATTRIBUTES | SYNCHRONIZE;

    hDir = CreateFile(FilePath->StartOfString,
                      AccessRequired,
                      FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
                      NULL,
                      OPEN_EXISTING,
                      FILE_FLAG_BACKUP_SEMANTICS,
                      NULL);

    if (hDir == INVALID_HANDLE_VALUE) {
        return FALSE;
    }

    CaseSensitiveInfo.Flags = 0;
    if (DirCaseContext->CaseSensitive) {
        CaseSensitiveInfo.Flags = 1;
    }


    Status = DllNtDll.pNtSetInformationFile(hDir, &IoStatusBlock, &CaseSensitiveInfo, sizeof(CaseSensitiveInfo), FileCaseSensitiveInformation);
    CloseHandle(hDir);

    if (Status != 0) {
        LPTSTR ErrText = YoriLibGetNtErrorText(Status);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Update of %y failed: %s"), FilePath, ErrText);
        YoriLibFreeWinErrorText(ErrText);
    } else {
        DirCaseContext->DirsModified++;
    }

    return TRUE;
}

/**
 A callback that is invoked when a directory cannot be successfully enumerated.

 @param FilePath Pointer to the file path that could not be enumerated.

 @param ErrorCode The Win32 error code describing the failure.

 @param Depth Recursion depth, ignored in this application.

 @param Context Pointer to the context block indicating whether the
        enumeration was recursive.  Recursive enumerates do not complain
        if a matching file is not in every single directory, because
        common usage expects files to be in a subset of directories only.

 @return TRUE to continute enumerating, FALSE to abort.
 */
BOOL
DirCaseFileEnumerateErrorCallback(
    __in PYORI_STRING FilePath,
    __in DWORD ErrorCode,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    YORI_STRING UnescapedFilePath;
    BOOL Result = FALSE;
    PDIRCASE_CONTEXT DirCaseContext = (PDIRCASE_CONTEXT)Context;

    UNREFERENCED_PARAMETER(Depth);

    YoriLibInitEmptyString(&UnescapedFilePath);
    if (!YoriLibUnescapePath(FilePath, &UnescapedFilePath)) {
        UnescapedFilePath.StartOfString = FilePath->StartOfString;
        UnescapedFilePath.LengthInChars = FilePath->LengthInChars;
    }

    if (ErrorCode == ERROR_FILE_NOT_FOUND || ErrorCode == ERROR_PATH_NOT_FOUND) {
        if (!DirCaseContext->Recursive) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("File or directory not found: %y\n"), &UnescapedFilePath);
        }
        Result = TRUE;
    } else {
        LPTSTR ErrText = YoriLibGetWinErrorText(ErrorCode);
        YORI_STRING DirName;
        LPTSTR FilePart;
        YoriLibInitEmptyString(&DirName);
        DirName.StartOfString = UnescapedFilePath.StartOfString;
        FilePart = YoriLibFindRightMostCharacter(&UnescapedFilePath, '\\');
        if (FilePart != NULL) {
            DirName.LengthInChars = (DWORD)(FilePart - DirName.StartOfString);
        } else {
            DirName.LengthInChars = UnescapedFilePath.LengthInChars;
        }
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Enumerate of %y failed: %s"), &DirName, ErrText);
        YoriLibFreeWinErrorText(ErrText);
    }
    YoriLibFreeStringContents(&UnescapedFilePath);
    return Result;
}


#ifdef YORI_BUILTIN
/**
 The main entrypoint for the dircase builtin command.
 */
#define ENTRYPOINT YoriCmd_YDIRCASE
#else
/**
 The main entrypoint for the dircase standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the dircase cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process, zero on success, nonzero on failure.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    DWORD MatchFlags;
    BOOL BasicEnumeration = FALSE;
    BOOLEAN OperationFound;
    DIRCASE_CONTEXT DirCaseContext;
    YORI_STRING Arg;
    DWORD MajorVersion;
    DWORD MinorVersion;
    DWORD BuildNumber;

    ZeroMemory(&DirCaseContext, sizeof(DirCaseContext));
    OperationFound = FALSE;

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                DirCaseHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2021"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("b")) == 0) {
                BasicEnumeration = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("ci")) == 0) {
                DirCaseContext.CaseSensitive = FALSE;
                OperationFound = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("cs")) == 0) {
                DirCaseContext.CaseSensitive = TRUE;
                OperationFound = TRUE;
                ArgumentUnderstood = TRUE;

            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("s")) == 0) {
                DirCaseContext.Recursive = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("v")) == 0) {
                DirCaseContext.Verbose = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                ArgumentUnderstood = TRUE;
                StartArg = i + 1;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    if (StartArg == 0 || StartArg == ArgC) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("dircase: missing argument\n"));
        return EXIT_FAILURE;
    }

    YoriLibGetOsVersion(&MajorVersion, &MinorVersion, &BuildNumber);

    if (DllNtDll.pNtSetInformationFile == NULL ||
        BuildNumber < 17763) {

        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("dircase: OS support not present\n"));
        return EXIT_FAILURE;
    }

    if (!OperationFound) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("dircase: operation not specified\n"));
        return EXIT_FAILURE;
    }

#if YORI_BUILTIN
    YoriLibCancelEnable();
#endif

    MatchFlags = YORILIB_FILEENUM_RETURN_DIRECTORIES;
    if (DirCaseContext.Recursive) {
        MatchFlags |= YORILIB_FILEENUM_RECURSE_BEFORE_RETURN;
    }
    if (BasicEnumeration) {
        MatchFlags |= YORILIB_FILEENUM_BASIC_EXPANSION;
    }

    for (i = StartArg; i < ArgC; i++) {

        YoriLibForEachFile(&ArgV[i],
                           MatchFlags,
                           0,
                           DirCaseFileFoundCallback,
                           DirCaseFileEnumerateErrorCallback,
                           &DirCaseContext);
    }

    if (DirCaseContext.DirsFound == 0) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("dircase: no matching files found\n"));
    }

    if (DirCaseContext.DirsModified == 0) {
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>hilite/hilite.c<|end_filename|>
/**
 * @file hilite/hilite.c
 *
 * Yori shell highlight lines or text in an input stream
 *
 * Copyright (c) 2018-2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 Help text to display to the user.
 */
const
CHAR strHiliteHelpText[] =
        "\n"
        "Output the contents of one or more files with highlight on lines\n"
        "or text matching specified criteria.\n"
        "\n"
        "HILITE [-license] [-b] [-c <string> <color>] [-h <string> <color>]\n"
        "       [-i] [-m] [-s] [-t <string> <color>] [<file>...]\n"
        "\n"
        "   -b             Use basic search criteria for files only\n"
        "   -c             Highlight lines containing <string> with <color>\n"
        "   -h             Highlight lines starting with <string> with <color>\n"
        "   -i             Match insensitively\n"
        "   -m             Highlight matching text (as opposed to matching lines)\n"
        "   -s             Process files from all subdirectories\n"
        "   -t             Highlight lines ending with <string> with <color>\n";

/**
 Display usage text to the user.
 */
BOOL
HiliteHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Hilite %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strHiliteHelpText);
    return TRUE;
}

/**
 The types of matches this program supports.
 */
typedef enum _HILITE_MATCH_TYPE {
    HiliteMatchTypeBeginsWith = 1,
    HiliteMatchTypeEndsWith = 2,
    HiliteMatchTypeContains = 3
} HILITE_MATCH_TYPE;

/**
 Context information for a specific match, including the criteria, the string
 to match against, and the color to apply if the match matches.
 */
typedef struct _HILITE_MATCH_CRITERIA {

    /**
     The list of matches to apply.
     */
    YORI_LIST_ENTRY ListEntry;

    /**
     The type of comparison to apply to determine a match.
     */
    HILITE_MATCH_TYPE MatchType;

    /**
     A string to compare with to determine a match.
     */
    YORI_STRING MatchString;

    /**
     The color to apply to the line, in event of a match.
     */
    YORILIB_COLOR_ATTRIBUTES Color;
} HILITE_MATCH_CRITERIA, *PHILITE_MATCH_CRITERIA;

/**
 Context passed to the callback which is invoked for each file found.
 */
typedef struct _HILITE_CONTEXT {

    /**
     Records the total number of files processed.
     */
    LONGLONG FilesFound;

    /**
     TRUE if matches should be applied case insensitively, FALSE if they
     should be applied case sensitively.
     */
    BOOLEAN Insensitive;

    /**
     TRUE if any text matching the criteria should be highlighted.  FALSE if
     the entire line should be highlighted.
     */
    BOOLEAN HighlightMatchText;

    /**
     TRUE if file enumeration is being performed recursively; FALSE if it is
     in one directory only.
     */
    BOOLEAN Recursive;

    /**
     The color to apply if none of the matches match.
     */
    YORILIB_COLOR_ATTRIBUTES DefaultColor;

    /**
     A list of matches to apply against the beginning of lines.
     */
    YORI_LIST_ENTRY StartMatches;

    /**
     A list of matches to apply against the middle of lines.
     */
    YORI_LIST_ENTRY MiddleMatches;

    /**
     A list of matches to apply against the end of lines.
     */
    YORI_LIST_ENTRY EndMatches;

} HILITE_CONTEXT, *PHILITE_CONTEXT;

/**
 Return the next match, in order.  All matches that must be at the start
 of the line are returned first, then all matches in the middle of the line,
 then all matches at the end of the line.

 @param HiliteContext Pointer to the context.

 @param ListContext Pointer to a variable that on input contains the list to
        start navigating from, and on completion contains the list to navigate
        next.

 @param PreviousMatch Pointer to the previously returned match, or NULL to
        start matching from the start.

 @return Pointer to the next match.
 */
PHILITE_MATCH_CRITERIA
HiliteGetNextMatch(
    __in PHILITE_CONTEXT HiliteContext,
    __in PYORI_LIST_ENTRY *ListContext,
    __in_opt PHILITE_MATCH_CRITERIA PreviousMatch
    )
{
    PYORI_LIST_ENTRY ListHead;
    PYORI_LIST_ENTRY ListEntry;
    PYORI_LIST_ENTRY PreviousEntry;
    PHILITE_MATCH_CRITERIA Match;

    ListHead = *ListContext;
    if (ListHead == NULL) {
        ListHead = &HiliteContext->StartMatches;
    }

    if (PreviousMatch != NULL) {
        PreviousEntry = &PreviousMatch->ListEntry;
    } else {
        PreviousEntry = NULL;
    }

    ListEntry = NULL;
    while(TRUE) {
        ListEntry = YoriLibGetNextListEntry(ListHead, PreviousEntry);
        if (ListEntry != NULL) {
            break;
        }

        if (ListHead == &HiliteContext->StartMatches) {
            ListHead = &HiliteContext->MiddleMatches;
        } else if (ListHead == &HiliteContext->MiddleMatches) {
            ListHead = &HiliteContext->EndMatches;
        } else {
            ListEntry = NULL;
            break;
        }

        PreviousEntry = NULL;
        *ListContext = ListHead;
    }

    if (ListEntry != NULL) {
        Match = CONTAINING_RECORD(ListEntry, HILITE_MATCH_CRITERIA, ListEntry);
        return Match;
    }

    return NULL;
}

/**
 Process a stream and apply the hilite criteria before outputting to standard
 output.

 @param hSource The incoming stream to highlight.

 @param HiliteContext Pointer to a set of criteria to apply to the stream
        before outputting.

 @return TRUE for success, FALSE on failure.
 */
BOOL
HiliteProcessStream(
    __in HANDLE hSource,
    __in PHILITE_CONTEXT HiliteContext
    )
{
    PVOID LineContext = NULL;
    CONSOLE_SCREEN_BUFFER_INFO ScreenInfo;
    YORI_STRING LineString;
    YORI_STRING Substring;
    YORI_STRING DisplayString;
    PHILITE_MATCH_CRITERIA MatchCriteria;
    PHILITE_MATCH_CRITERIA BestMatchCriteria;
    DWORD BestMatchOffset;
    YORILIB_COLOR_ATTRIBUTES ColorToUse;
    PYORI_LIST_ENTRY ListHead;
    BOOLEAN MatchFound;
    BOOLEAN AnyMatchFound;
    DWORD MatchOffset;

    YoriLibInitEmptyString(&LineString);
    YoriLibInitEmptyString(&Substring);
    YoriLibInitEmptyString(&DisplayString);
    MatchOffset = 0;

    HiliteContext->FilesFound++;

    while (TRUE) {

        if (!YoriLibReadLineToString(&LineString, &LineContext, hSource)) {
            break;
        }

        Substring.StartOfString = LineString.StartOfString;
        Substring.LengthInChars = LineString.LengthInChars;
        ColorToUse.Ctrl = HiliteContext->DefaultColor.Ctrl;
        ColorToUse.Win32Attr = HiliteContext->DefaultColor.Win32Attr;

        while (Substring.LengthInChars > 0) {

            //
            //  Enumerate through the matches and see if there is anything to
            //  apply.  At the start of the line, enumerate the matches to
            //  the beginning of lines; after that enumerate the matches that
            //  could be in the middle of a line.
            //

            BestMatchCriteria = NULL;
            BestMatchOffset = 0;
            AnyMatchFound = FALSE;
            if (Substring.StartOfString == LineString.StartOfString) {
                ListHead = &HiliteContext->StartMatches;
            } else {
                ListHead = &HiliteContext->MiddleMatches;
            }
            MatchCriteria = NULL;
            MatchCriteria = HiliteGetNextMatch(HiliteContext, &ListHead, MatchCriteria);
            while (MatchCriteria != NULL) {
                MatchFound = FALSE;
                if (MatchCriteria->MatchType == HiliteMatchTypeBeginsWith) {
                    if (HiliteContext->Insensitive) {
                        if (YoriLibCompareStringInsensitiveCount(&Substring,
                                                                 &MatchCriteria->MatchString,
                                                                 MatchCriteria->MatchString.LengthInChars) == 0) {
                            MatchFound = TRUE;
                            MatchOffset = 0;
                        }
                    } else {
                        if (YoriLibCompareStringCount(&Substring,
                                                      &MatchCriteria->MatchString,
                                                      MatchCriteria->MatchString.LengthInChars) == 0) {
                            MatchFound = TRUE;
                            MatchOffset = 0;
                        }
                    }
                } else if (MatchCriteria->MatchType == HiliteMatchTypeEndsWith) {
                    YORI_STRING TailOfLine;

                    if (Substring.LengthInChars >= MatchCriteria->MatchString.LengthInChars) {
                        YoriLibInitEmptyString(&TailOfLine);
                        TailOfLine.LengthInChars = MatchCriteria->MatchString.LengthInChars;
                        TailOfLine.StartOfString = &Substring.StartOfString[Substring.LengthInChars - MatchCriteria->MatchString.LengthInChars];

                        if (HiliteContext->Insensitive) {
                            if (YoriLibCompareStringInsensitive(&TailOfLine, &MatchCriteria->MatchString) == 0) {
                                MatchFound = TRUE;
                                MatchOffset = Substring.LengthInChars - MatchCriteria->MatchString.LengthInChars;
                            }
                        } else {
                            if (YoriLibCompareString(&TailOfLine, &MatchCriteria->MatchString) == 0) {
                                MatchFound = TRUE;
                                MatchOffset = Substring.LengthInChars - MatchCriteria->MatchString.LengthInChars;
                            }
                        }
                    }
                } else if (MatchCriteria->MatchType == HiliteMatchTypeContains) {
                    if (HiliteContext->Insensitive) {
                        if (YoriLibFindFirstMatchingSubstringInsensitive(&Substring, 1, &MatchCriteria->MatchString, &MatchOffset)) {
                            MatchFound = TRUE;
                        }
                    } else {
                        if (YoriLibFindFirstMatchingSubstring(&Substring, 1, &MatchCriteria->MatchString, &MatchOffset)) {
                            MatchFound = TRUE;
                        }
                    }
                }


                //
                //  When highlighting specific terms, look for the first match
                //  within the line.  That string should be processed first.
                //  When not matching specific terms, just use the first found
                //  match to highlight the entire line.
                //

                if (MatchFound) {

                    if (!HiliteContext->HighlightMatchText) {
                        BestMatchCriteria = MatchCriteria;
                        BestMatchOffset = MatchOffset;
                        break;
                    }

                    if (MatchCriteria->MatchString.LengthInChars > 0 &&
                        (BestMatchCriteria == NULL || MatchOffset < BestMatchOffset)) {
                        BestMatchCriteria = MatchCriteria;
                        BestMatchOffset = MatchOffset;
                    }
                }

                MatchCriteria = HiliteGetNextMatch(HiliteContext, &ListHead, MatchCriteria);
            }

            //
            //  If this is highlighting a search term only, display any
            //  text before the match in regular color, then display the
            //  match in the requested color.  If highlighting the whole
            //  line, display all the text.  Then start searching again,
            //  from all entries that can be in the middle of lines.
            //

            DisplayString.StartOfString = Substring.StartOfString;
            DisplayString.LengthInChars = Substring.LengthInChars;
            if (BestMatchCriteria != NULL) {
                AnyMatchFound = TRUE;
                if (HiliteContext->HighlightMatchText) {
                    if (BestMatchOffset > 0) {
                        DisplayString.LengthInChars = BestMatchOffset;
                        YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%y"), &DisplayString);
                        DisplayString.StartOfString = &Substring.StartOfString[BestMatchOffset];
                        Substring.LengthInChars = Substring.LengthInChars - BestMatchOffset;
                        Substring.StartOfString = &Substring.StartOfString[BestMatchOffset];
                    }
                    DisplayString.LengthInChars = BestMatchCriteria->MatchString.LengthInChars;
                    //
                    //  If searching for an empty string, treat it as not
                    //  found and move to the next line.  This is only
                    //  done when highlighting specific text; when
                    //  highlighting an entire line, the empty string
                    //  matches the line, and execution continues on the
                    //  next line.
                    //

                    if (DisplayString.LengthInChars == 0) {
                        ASSERT(BestMatchOffset == 0);
                        break;
                    }
                    ColorToUse.Ctrl = BestMatchCriteria->Color.Ctrl;
                    ColorToUse.Win32Attr = BestMatchCriteria->Color.Win32Attr;
                } else {
                    ColorToUse.Ctrl = BestMatchCriteria->Color.Ctrl;
                    ColorToUse.Win32Attr = BestMatchCriteria->Color.Win32Attr;
                }

                YoriLibVtSetConsoleTextAttribute(YORI_LIB_OUTPUT_STDOUT, ColorToUse.Win32Attr);
                YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%y"), &DisplayString);
                YoriLibVtSetConsoleTextAttribute(YORI_LIB_OUTPUT_STDOUT, HiliteContext->DefaultColor.Win32Attr);
                Substring.StartOfString = &Substring.StartOfString[DisplayString.LengthInChars];
                Substring.LengthInChars = Substring.LengthInChars - DisplayString.LengthInChars;
            }

            //
            //  If all matches have been navigated and the string hasn't
            //  changed, no more matches were found, so move to the next line.
            //

            if (!AnyMatchFound) {
                break;
            }
        }

        //
        //  If no matches are found, display the line.
        //

        YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%y"), &Substring);

        //
        //  Apply a newline if needed.
        //

        if (LineString.LengthInChars == 0 || !GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &ScreenInfo) || ScreenInfo.dwCursorPosition.X != 0) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("\n"));
        }
    }

    YoriLibLineReadCloseOrCache(LineContext);
    YoriLibFreeStringContents(&LineString);

    return TRUE;
}

/**
 A callback that is invoked when a file is found that matches a search criteria
 specified in the set of strings to enumerate.

 @param FilePath Pointer to the file path that was found.

 @param FileInfo Information about the file.

 @param Depth Recursion depth, ignored in this application.

 @param Context Pointer to the hilite context structure indicating the
        action to perform and populated with the file and line count found.

 @return TRUE to continute enumerating, FALSE to abort.
 */
BOOL
HiliteFileFoundCallback(
    __in PYORI_STRING FilePath,
    __in PWIN32_FIND_DATA FileInfo,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    HANDLE FileHandle;
    PHILITE_CONTEXT HiliteContext = (PHILITE_CONTEXT)Context;

    UNREFERENCED_PARAMETER(Depth);

    ASSERT(YoriLibIsStringNullTerminated(FilePath));

    if ((FileInfo->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) {
        FileHandle = CreateFile(FilePath->StartOfString,
                                GENERIC_READ,
                                FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                                NULL,
                                OPEN_EXISTING,
                                FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS,
                                NULL);

        if (FileHandle == NULL || FileHandle == INVALID_HANDLE_VALUE) {
            DWORD LastError = GetLastError();
            LPTSTR ErrText = YoriLibGetWinErrorText(LastError);
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("hilite: open of %y failed: %s"), FilePath, ErrText);
            YoriLibFreeWinErrorText(ErrText);
            return TRUE;
        }

        HiliteProcessStream(FileHandle, HiliteContext);

        CloseHandle(FileHandle);
    }

    return TRUE;
}

/**
 A callback that is invoked when a directory cannot be successfully enumerated.

 @param FilePath Pointer to the file path that could not be enumerated.

 @param ErrorCode The Win32 error code describing the failure.

 @param Depth Recursion depth, ignored in this application.

 @param Context Pointer to the context block indicating whether the
        enumeration was recursive.  Recursive enumerates do not complain
        if a matching file is not in every single directory, because
        common usage expects files to be in a subset of directories only.

 @return TRUE to continute enumerating, FALSE to abort.
 */
BOOL
HiliteFileEnumerateErrorCallback(
    __in PYORI_STRING FilePath,
    __in DWORD ErrorCode,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    YORI_STRING UnescapedFilePath;
    PHILITE_CONTEXT HiliteContext = (PHILITE_CONTEXT)Context;
    BOOL Result = FALSE;

    UNREFERENCED_PARAMETER(Depth);

    YoriLibInitEmptyString(&UnescapedFilePath);
    if (!YoriLibUnescapePath(FilePath, &UnescapedFilePath)) {
        UnescapedFilePath.StartOfString = FilePath->StartOfString;
        UnescapedFilePath.LengthInChars = FilePath->LengthInChars;
    }

    if (ErrorCode == ERROR_FILE_NOT_FOUND || ErrorCode == ERROR_PATH_NOT_FOUND) {
        if (!HiliteContext->Recursive) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("File or directory not found: %y\n"), &UnescapedFilePath);
        }
        Result = TRUE;
    } else {
        LPTSTR ErrText = YoriLibGetWinErrorText(ErrorCode);
        YORI_STRING DirName;
        LPTSTR FilePart;
        YoriLibInitEmptyString(&DirName);
        DirName.StartOfString = UnescapedFilePath.StartOfString;
        FilePart = YoriLibFindRightMostCharacter(&UnescapedFilePath, '\\');
        if (FilePart != NULL) {
            DirName.LengthInChars = (DWORD)(FilePart - DirName.StartOfString);
        } else {
            DirName.LengthInChars = UnescapedFilePath.LengthInChars;
        }
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Enumerate of %y failed: %s"), &DirName, ErrText);
        YoriLibFreeWinErrorText(ErrText);
    }
    YoriLibFreeStringContents(&UnescapedFilePath);
    return Result;
}

/**
 Deallocate any user specified hilite criteria.

 @param HiliteContext The context from which all user specified criteria
        should be deallocated.
 */
VOID
HiliteCleanupContext(
    __in PHILITE_CONTEXT HiliteContext
    )
{
    PHILITE_MATCH_CRITERIA MatchCriteria;
    PHILITE_MATCH_CRITERIA NextMatchCriteria;
    PYORI_LIST_ENTRY ListHead;

    ListHead = &HiliteContext->StartMatches;

    MatchCriteria = HiliteGetNextMatch(HiliteContext, &ListHead, NULL);
    while (MatchCriteria != NULL) {
        NextMatchCriteria = HiliteGetNextMatch(HiliteContext, &ListHead, MatchCriteria);
        YoriLibRemoveListItem(&MatchCriteria->ListEntry);
        YoriLibFree(MatchCriteria);
        MatchCriteria = NextMatchCriteria;
    }
}


#ifdef YORI_BUILTIN
/**
 The main entrypoint for the hilite builtin command.
 */
#define ENTRYPOINT YoriCmd_HILITE
#else
/**
 The main entrypoint for the hilite standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the hilite cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the child process on success, or failure if the child
         could not be launched.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    DWORD MatchFlags;
    BOOL BasicEnumeration = FALSE;
    HILITE_CONTEXT HiliteContext;
    CONSOLE_SCREEN_BUFFER_INFO ScreenInfo;
    PHILITE_MATCH_CRITERIA NewCriteria;
    YORI_STRING Arg;

    ZeroMemory(&HiliteContext, sizeof(HiliteContext));

    if (GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &ScreenInfo)) {
        HiliteContext.DefaultColor.Ctrl = 0;
        HiliteContext.DefaultColor.Win32Attr = (UCHAR)ScreenInfo.wAttributes;
    } else {
        HiliteContext.DefaultColor.Ctrl = 0;
        HiliteContext.DefaultColor.Win32Attr = 0x7;
    }

    YoriLibInitializeListHead(&HiliteContext.StartMatches);
    YoriLibInitializeListHead(&HiliteContext.MiddleMatches);
    YoriLibInitializeListHead(&HiliteContext.EndMatches);

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                HiliteHelp();
                HiliteCleanupContext(&HiliteContext);
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2018-2021"));
                HiliteCleanupContext(&HiliteContext);
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("b")) == 0) {
                BasicEnumeration = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("c")) == 0) {
                if (i + 2 < ArgC) {
                    NewCriteria = YoriLibMalloc(sizeof(HILITE_MATCH_CRITERIA));
                    if (NewCriteria == NULL) {
                        HiliteCleanupContext(&HiliteContext);
                        return EXIT_FAILURE;
                    }
                    NewCriteria->MatchType = HiliteMatchTypeContains;
                    YoriLibInitEmptyString(&NewCriteria->MatchString);
                    NewCriteria->MatchString.StartOfString = ArgV[i + 1].StartOfString;
                    NewCriteria->MatchString.LengthInChars = ArgV[i + 1].LengthInChars;
                    YoriLibAttributeFromLiteralString(ArgV[i + 2].StartOfString, &NewCriteria->Color);
                    YoriLibResolveWindowColorComponents(NewCriteria->Color, HiliteContext.DefaultColor, FALSE, &NewCriteria->Color);
                    YoriLibAppendList(&HiliteContext.MiddleMatches, &NewCriteria->ListEntry);
                    ArgumentUnderstood = TRUE;
                    i += 2;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("h")) == 0) {
                if (i + 2 < ArgC) {
                    NewCriteria = YoriLibMalloc(sizeof(HILITE_MATCH_CRITERIA));
                    if (NewCriteria == NULL) {
                        HiliteCleanupContext(&HiliteContext);
                        return EXIT_FAILURE;
                    }
                    NewCriteria->MatchType = HiliteMatchTypeBeginsWith;
                    YoriLibInitEmptyString(&NewCriteria->MatchString);
                    NewCriteria->MatchString.StartOfString = ArgV[i + 1].StartOfString;
                    NewCriteria->MatchString.LengthInChars = ArgV[i + 1].LengthInChars;
                    YoriLibAttributeFromLiteralString(ArgV[i + 2].StartOfString, &NewCriteria->Color);
                    YoriLibResolveWindowColorComponents(NewCriteria->Color, HiliteContext.DefaultColor, FALSE, &NewCriteria->Color);
                    YoriLibAppendList(&HiliteContext.StartMatches, &NewCriteria->ListEntry);
                    ArgumentUnderstood = TRUE;
                    i += 2;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("i")) == 0) {
                HiliteContext.Insensitive = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("m")) == 0) {
                HiliteContext.HighlightMatchText = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("s")) == 0) {
                HiliteContext.Recursive = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("t")) == 0) {
                if (i + 2 < ArgC) {
                    NewCriteria = YoriLibMalloc(sizeof(HILITE_MATCH_CRITERIA));
                    if (NewCriteria == NULL) {
                        HiliteCleanupContext(&HiliteContext);
                        return EXIT_FAILURE;
                    }
                    NewCriteria->MatchType = HiliteMatchTypeEndsWith;
                    YoriLibInitEmptyString(&NewCriteria->MatchString);
                    NewCriteria->MatchString.StartOfString = ArgV[i + 1].StartOfString;
                    NewCriteria->MatchString.LengthInChars = ArgV[i + 1].LengthInChars;
                    YoriLibAttributeFromLiteralString(ArgV[i + 2].StartOfString, &NewCriteria->Color);
                    YoriLibResolveWindowColorComponents(NewCriteria->Color, HiliteContext.DefaultColor, FALSE, &NewCriteria->Color);
                    YoriLibAppendList(&HiliteContext.EndMatches, &NewCriteria->ListEntry);
                    ArgumentUnderstood = TRUE;
                    i += 2;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                StartArg = i + 1;
                ArgumentUnderstood = TRUE;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    //
    //  Attempt to enable backup privilege so an administrator can access more
    //  objects successfully.
    //

    YoriLibEnableBackupPrivilege();

    //
    //  If no file name is specified, use stdin; otherwise open
    //  the file and use that
    //

    if (StartArg == 0 || StartArg == ArgC) {
        if (YoriLibIsStdInConsole()) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("No file or pipe for input\n"));
            HiliteCleanupContext(&HiliteContext);
            return EXIT_FAILURE;
        }

        HiliteProcessStream(GetStdHandle(STD_INPUT_HANDLE), &HiliteContext);
    } else {
        MatchFlags = YORILIB_FILEENUM_RETURN_FILES | YORILIB_FILEENUM_DIRECTORY_CONTENTS;
        if (HiliteContext.Recursive) {
            MatchFlags |= YORILIB_FILEENUM_RECURSE_BEFORE_RETURN | YORILIB_FILEENUM_RECURSE_PRESERVE_WILD;
        }
        if (BasicEnumeration) {
            MatchFlags |= YORILIB_FILEENUM_BASIC_EXPANSION;
        }

        for (i = StartArg; i < ArgC; i++) {

            YoriLibForEachStream(&ArgV[i],
                                 MatchFlags,
                                 0,
                                 HiliteFileFoundCallback,
                                 HiliteFileEnumerateErrorCallback,
                                 &HiliteContext);
        }
    }

    HiliteCleanupContext(&HiliteContext);

#if !YORI_BUILTIN
    YoriLibLineReadCleanupCache();
#endif

    if (HiliteContext.FilesFound == 0) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("hilite: no matching files found\n"));
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>err/err.c<|end_filename|>
/**
 * @file err/err.c
 *
 * Yori shell display windows error codes as strings
 *
 * Copyright (c) 2019 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 Help text to display to the user.
 */
const
CHAR strErrHelpText[] =
        "\n"
        "Display error codes as strings.\n"
        "\n"
        "ERR [-license] [-n|-s|-w] <error>\n"
        "\n"
        "   -n             The error refers to an NTSTATUS code\n"
        "   -s             The error refers to an NTSTATUS code\n"
        "   -w             The error refers to a Win32 error code\n";

/**
 Display usage text to the user.
 */
BOOL
ErrHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Err %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strErrHelpText);
    return TRUE;
}

/**
 Heuristically guess if a number is hexadecimal.  Errors often take this form
 and demanding an 0x prefix for each of them seems inconvenient and counter-
 intuitive.  Note that a user can explicitly override this function in either
 direction with 0x or 0n prefixes.

 @param NumberString Pointer to the number in string form to check.

 @return TRUE to indicate the number should be treated as a hex number, FALSE
         to indicate that regular number parsing should be applied, which
         defaults to decimal.
 */
BOOL
ErrIsNumberProbablyHex(
    __in PYORI_STRING NumberString
    )
{
    DWORD Index;

    //
    //  An 8 character string is probably hex, because that corresponds to
    //  a 32 bit number, and both NTSTATUS and HRESULT codes indicate an
    //  error by setting the high bit so the entire 32 bits needs to be
    //  specified.  Win32 doesn't require the high bit set, but is also
    //  not conventionally specified as hex.
    //

    if (NumberString->LengthInChars != sizeof(DWORD) * 2) {
        return FALSE;
    }

    //
    //  This is checking if the number is a legal hex number, but what it's
    //  really looking for is an explicit prefix indicating it's some other
    //  type of number.
    //

    for (Index = 0; Index < NumberString->LengthInChars; Index++) {
        if ((NumberString->StartOfString[Index] < '0' || NumberString->StartOfString[Index] > '9') &&
            (NumberString->StartOfString[Index] < 'a' || NumberString->StartOfString[Index] > 'f') &&
            (NumberString->StartOfString[Index] < 'A' || NumberString->StartOfString[Index] > 'F')) {
            return FALSE;
        }
    }

    return TRUE;
}

/**
 The type of the error whose string should be located.
 */
typedef enum {
    ErrTypeUnknown = 0,
    ErrTypeWindows = 1,
    ErrTypeNtstatus = 2
} ERR_TYPE;

#ifdef YORI_BUILTIN
/**
 The main entrypoint for the err builtin command.
 */
#define ENTRYPOINT YoriCmd_YERR
#else
/**
 The main entrypoint for the err standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the err cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process, zero indicating success or nonzero on
         failure.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    YORI_STRING Arg;
    LONGLONG llTemp;
    DWORD CharsConsumed;
    DWORD Error;
    ERR_TYPE ErrType;
    LPTSTR ErrText;

    ErrType = ErrTypeUnknown;

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                ErrHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2019"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("n")) == 0) {
                ErrType = ErrTypeNtstatus;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("s")) == 0) {
                ErrType = ErrTypeNtstatus;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("w")) == 0) {
                ErrType = ErrTypeWindows;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitiveCount(&Arg, _T("0"), 1) >= 0 &&
                       YoriLibCompareStringWithLiteralInsensitiveCount(&Arg, _T("9"), 1) <= 0) {
                StartArg = i;
                ArgumentUnderstood = TRUE;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    if (StartArg == 0 || StartArg >= ArgC) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("err: Missing argument\n"));
        return EXIT_FAILURE;
    }

    if (ErrIsNumberProbablyHex(&ArgV[StartArg])) {
        if (!YoriLibStringToNumberSpecifyBase(&ArgV[StartArg], 16, TRUE, &llTemp, &CharsConsumed) || CharsConsumed == 0) {
            if (!YoriLibStringToNumber(&ArgV[StartArg], TRUE, &llTemp, &CharsConsumed) || CharsConsumed == 0) {
                YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("err: Argument not understood: %y\n"), &ArgV[StartArg]);
                return EXIT_FAILURE;
            }
        }
    } else if (!YoriLibStringToNumber(&ArgV[StartArg], TRUE, &llTemp, &CharsConsumed) || CharsConsumed == 0) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("err: Argument not understood: %y\n"), &ArgV[StartArg]);
        return EXIT_FAILURE;
    }

    Error = (DWORD)llTemp;
    ErrText = NULL;

    if (ErrType == ErrTypeUnknown) {
        if (Error >= 0x80000000) {
            ErrType = ErrTypeNtstatus;
        } else {
            ErrType = ErrTypeWindows;
        }
    }

    switch(ErrType) {
        case ErrTypeWindows:
            ErrText = YoriLibGetWinErrorText(Error);
            break;
        case ErrTypeNtstatus:
            ErrText = YoriLibGetNtErrorText(Error);
            break;
    }

    if (ErrText != NULL) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%s"), ErrText);
        YoriLibFreeWinErrorText(ErrText);
    } else {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Could not fetch error text.\n"));
    }

    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>which/which.c<|end_filename|>
/**
 * @file which/which.c
 *
 * Small minicrt wrapper app that searches the path or other environment
 * variable looking for the first matching file.
 *
 * Copyright (c) 2014-2018 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 The string to search for.
 */
PYORI_STRING SearchFor = NULL;

/**
 The variable to search in.
 */
LPTSTR SearchVar = _T("PATH");

/**
 Usage text for this application.
 */
const
CHAR strWhichUsageText[] =
     "\n"
     "Searches a semicolon delimited environment variable for a file.  When\n"
     "searching PATH, also applies PATHEXT executable extension matching.\n"
     "\n"
     "WHICH [-license] [-p <variable>] <file>\n"
     "\n"
     "   -p var Indicates the environment variable to search.  If not specified, use PATH\n"
     "\n"
     " If PATHEXT not defined, defaults to .COM, .EXE, .BAT and .CMD\n"
     " If file extension not specified and var not specified, searches for files\n"
     "  ending in extensions in PATHEXT\n";

/**
 Display usage text to the user.
 */
VOID
WhichUsage(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Which %i.%02i\n"), WHICH_VER_MAJOR, WHICH_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif

    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strWhichUsageText);
}

/**
 Parse user specified arguments.

 @param ArgC The count of arguments.

 @param ArgV Array of arguments.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
WhichParseArgs(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    DWORD i;
    YORI_STRING Arg;

    for (i = 1; i < ArgC; i++) {
        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {
            BOOL Parsed = FALSE;

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2014-2018"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("p")) == 0 &&
                       ArgC > i + 1) {

                i++;
                SearchVar = ArgV[i].StartOfString;
                Parsed = TRUE;
            }

            if (!Parsed) {
                WhichUsage();
                return FALSE;
            }
        } else {
            SearchFor = &ArgV[i];
        }
    }

    if (SearchFor == NULL) {
        WhichUsage();
        return FALSE;
    }

    return TRUE;
}

#ifdef YORI_BUILTIN
/**
 The main entrypoint for the which builtin command.
 */
#define ENTRYPOINT YoriCmd_WHICH
#else
/**
 The main entrypoint for the which standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the which application.

 @param ArgC Count of arguments.

 @param ArgV Array of arguments.

 @return ExitCode, zero indicating success, nonzero indicating failure.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    YORI_STRING FoundPath;
    BOOL Result = FALSE;

    if (!WhichParseArgs(ArgC, ArgV)) {
        return EXIT_FAILURE;
    }

    //
    //  When running on WOW64, we don't want file system redirection,
    //  for path evaluation, because 32 bit processes will execute
    //  from 64 bit paths.  For other variables, leave redirection
    //  in place.
    //

    if (DllKernel32.pWow64DisableWow64FsRedirection &&
        _tcsicmp(SearchVar, _T("PATH")) == 0) {

        PVOID DontCare;
        DllKernel32.pWow64DisableWow64FsRedirection(&DontCare);
    }

    YoriLibInitEmptyString(&FoundPath);

    if (_tcsicmp(SearchVar, _T("PATH")) == 0) {
        Result = YoriLibLocateExecutableInPath(SearchFor, NULL, NULL, &FoundPath);
    } else {
        DWORD VarLength;
        YORI_STRING SearchVarData;

        YoriLibInitEmptyString(&SearchVarData);
        VarLength = GetEnvironmentVariable(SearchVar, NULL, 0);

        if (VarLength > 0) {
            if (YoriLibAllocateString(&SearchVarData, VarLength)) {
                SearchVarData.LengthInChars = GetEnvironmentVariable(SearchVar, SearchVarData.StartOfString, SearchVarData.LengthAllocated);
                if (SearchVarData.LengthInChars == 0 || SearchVarData.LengthInChars >= SearchVarData.LengthAllocated) {
                    YoriLibFreeStringContents(&SearchVarData);
                }
            }
        }

        if (!YoriLibAllocateString(&FoundPath, VarLength + MAX_PATH)) {
            YoriLibFreeStringContents(&SearchVarData);
        }

        Result = FALSE;
        if (SearchVarData.StartOfString != NULL) {
            if (FoundPath.StartOfString != NULL) {
                Result = YoriLibPathLocateKnownExtensionUnknownLocation(SearchFor, &SearchVarData, NULL, NULL, &FoundPath);
            }
            YoriLibFreeStringContents(&SearchVarData);
        }
    }

    //
    //  Tell the user what we found, if anything
    //

    if (!Result) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Error performing search\n"));
        return EXIT_FAILURE;
    } else if (FoundPath.LengthInChars > 0) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%y\n"), &FoundPath);
        YoriLibFreeStringContents(&FoundPath);
        return EXIT_SUCCESS;
    } else {
        YoriLibFreeStringContents(&FoundPath);
        YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Not found\n"));
        return EXIT_FAILURE;
    }
}

// vim:sw=4:ts=4:et:


<|start_filename|>more/more.c<|end_filename|>
/**
 * @file more/more.c
 *
 * Yori shell display file contents
 *
 * Copyright (c) 2017-2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "more.h"

/**
 Help text to display to the user.
 */
const
CHAR strMoreHelpText[] =
        "\n"
        "Output the contents of one or more files with paging and scrolling.\n"
        "\n"
        "MORE [-license] [-b] [-dd] [-f] [-s] [<file>...]\n"
        "\n"
        "   -b             Use basic search criteria for files only\n"
        "   -dd            Use the debug display\n"
        "   -f             Wait for more contents to be added to the file\n"
        "   -l             Display until Ctrl+Q, Scroll Lock, or pause\n"
        "   -s             Process files from all subdirectories\n";

/**
 Display usage text to the user.
 */
BOOL
MoreHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("More %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strMoreHelpText);
    return TRUE;
}

#ifdef YORI_BUILTIN
/**
 The main entrypoint for the more builtin command.
 */
#define ENTRYPOINT YoriCmd_YMORE
#else
/**
 The main entrypoint for the more standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the more cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process, zero on success, nonzero on failure.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    DWORD CurrentMode;
    DWORD Result;
    BOOLEAN Recursive = FALSE;
    BOOLEAN BasicEnumeration = FALSE;
    BOOL InitComplete;
    BOOLEAN DebugDisplay = FALSE;
    BOOLEAN SuspendPagination = FALSE;
    BOOLEAN WaitForMore = FALSE;
    MORE_CONTEXT MoreContext;
    YORI_STRING Arg;

    ZeroMemory(&MoreContext, sizeof(MoreContext));

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                MoreHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2017-2021"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("b")) == 0) {
                BasicEnumeration = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("dd")) == 0) {
                DebugDisplay = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("f")) == 0) {
                WaitForMore = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("l")) == 0) {
                SuspendPagination = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("s")) == 0) {
                Recursive = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                StartArg = i + 1;
                ArgumentUnderstood = TRUE;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    if (!GetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), &CurrentMode)) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("more: output is not interactive console\n"));
        return EXIT_FAILURE;
    }

    //
    //  Attempt to enable backup privilege so an administrator can access more
    //  objects successfully.
    //

    YoriLibEnableBackupPrivilege();

    //
    //  Enabling cancel allows the process to terminate if it's waiting for
    //  input (so the pipe is active) but the user requests the process to
    //  exit.  This is needed for both builtin and non-builtin forms.
    //

    YoriLibCancelEnable();

    if (StartArg == 0 || StartArg == ArgC) {
        InitComplete = MoreInitContext(&MoreContext, 0, NULL, Recursive, BasicEnumeration, DebugDisplay, SuspendPagination, WaitForMore);
    } else {
        InitComplete = MoreInitContext(&MoreContext, ArgC-StartArg, &ArgV[StartArg], Recursive, BasicEnumeration, DebugDisplay, SuspendPagination, WaitForMore);
    }

    Result = EXIT_SUCCESS;
    if (!InitComplete) {
        MoreCleanupContext(&MoreContext);
        Result = EXIT_FAILURE;
    }

    if (Result == EXIT_SUCCESS) {
        MoreViewportDisplay(&MoreContext);
        MoreGracefulExit(&MoreContext);
    }

#if !YORI_BUILTIN
    YoriLibLineReadCleanupCache();
    YoriLibEmptyProcessClipboard();
#endif

    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>sh/parse.c<|end_filename|>
/**
 * @file /sh/parse.c
 *
 * Parses an expression into component pieces
 *
 * Copyright (c) 2014-2020 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "yori.h"

/**
 Expand any aliases in a command context, resolve any executable via path
 lookups, and return with an exec context indicating which program to run.
 If a program is found, ExecutableFound is set to TRUE on return.  If a 
 program is not found, the command should be considered a builtin.  This
 function does not validate if any such builtin exists.

 @param CmdContext The command context to resolve aliases and paths in.

 @param ExecutableFound On successful completion, indicates if an external
        executable was found (by being set to TRUE) or if the command should
        be considered a builtin (by being set to FALSE.)

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOL
YoriShResolveCommandToExecutable(
    __in PYORI_LIBSH_CMD_CONTEXT CmdContext,
    __out PBOOL ExecutableFound
    )
{
    YORI_STRING FoundExecutable;
    YORI_STRING ExpandedCmd;

    YoriLibInitEmptyString(&FoundExecutable);

    YoriShExpandAlias(CmdContext);

    if (!YoriLibExpandHomeDirectories(&CmdContext->ArgV[0], &ExpandedCmd)) {
        YoriLibCloneString(&ExpandedCmd, &CmdContext->ArgV[0]);
    }

    if (YoriLibLocateExecutableInPath(&ExpandedCmd, NULL, NULL, &FoundExecutable)) {

        if (FoundExecutable.LengthInChars > 0) {
            YoriLibFreeStringContents(&CmdContext->ArgV[0]);
            memcpy(&CmdContext->ArgV[0], &FoundExecutable, sizeof(YORI_STRING));
            *ExecutableFound = TRUE;
        } else {
            YoriLibFreeStringContents(&FoundExecutable);
            *ExecutableFound = FALSE;
        }
    } else {
        *ExecutableFound = FALSE;
    }

    YoriLibFreeStringContents(&ExpandedCmd);

    return TRUE;
}

/**
 Expand environment variables that remain in any argument of a command
 context.  Note these can refer to variables that are provided by the
 shell, so this functionality is not in a generic library.

 @param CmdContext Pointer to the command context.  On output, any arguments
        containing defined environment variables are replaced with their
        expanded form.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOLEAN
YoriShExpandEnvironmentInCmdContext(
    __inout PYORI_LIBSH_CMD_CONTEXT CmdContext
    )
{
    DWORD Index;
    DWORD ArgOffset;

    for (Index = 0; Index < CmdContext->ArgC; Index++) {
        YORI_STRING EnvExpandedString;
        ASSERT(YoriLibIsStringNullTerminated(&CmdContext->ArgV[Index]));

        ArgOffset = 0;
        if (Index == CmdContext->CurrentArg) {
            ArgOffset = CmdContext->CurrentArgOffset;
        }
        if (YoriShExpandEnvironmentVariables(&CmdContext->ArgV[Index], &EnvExpandedString, &ArgOffset)) {
            if (EnvExpandedString.StartOfString != CmdContext->ArgV[Index].StartOfString) {
                if (Index == CmdContext->CurrentArg) {
                    CmdContext->CurrentArgOffset = ArgOffset;
                }

                YoriLibFreeStringContents(&CmdContext->ArgV[Index]);
                memcpy(&CmdContext->ArgV[Index], &EnvExpandedString, sizeof(YORI_STRING));
                ASSERT(YoriLibIsStringNullTerminated(&CmdContext->ArgV[Index]));
            }
        }
    }

    return TRUE;
}


// vim:sw=4:ts=4:et:


<|start_filename|>setver/setver.c<|end_filename|>
/**
 * @file setver/setver.c
 *
 * Yori shell invoke a child process with an explicit Windows version number.
 *
 * Copyright (c) 2018-2019 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 Help text to display to the user.
 */
const
CHAR strSetVerHelpText[] =
        "\n"
        "Runs a child program with an explicit Windows version.\n"
        "\n"
        "SETVER [-license] <version> <command>\n";

/**
 Display usage text to the user.
 */
BOOL
SetVerHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("SetVer %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strSetVerHelpText);
    return TRUE;
}

/**
 Context to pass around between functions describing the operation to perform.
 */
typedef struct _SETVER_CONTEXT {

    /**
     The major version to report to applications.
     */
    DWORD AppVerMajor;

    /**
     The minor version to report to applications.
     */
    DWORD AppVerMinor;

    /**
     The build number to report to applications.
     */
    DWORD AppBuildNumber;

    /**
     Pointer to process information state about the initial process launched.
     */
    PPROCESS_INFORMATION ProcessInfo;
} SETVER_CONTEXT, *PSETVER_CONTEXT;

/**
 If TRUE, output information related to messages being processed by the
 debugger.
 */
#define SETVER_DEBUG 0

/**
 Apply the requested OS version into the PEB of an opened process.

 @param Context Pointer to the context describing the OS version to apply.

 @param hProcess Handle to the process to apply the OS version into.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SetVerApplyVersionToProcess(
    __in PSETVER_CONTEXT Context,
    __in HANDLE hProcess
    )
{
    BOOL TargetProcess32BitPeb;
    LONG Status;
    PROCESS_BASIC_INFORMATION BasicInfo;
    SIZE_T BytesReturned;
    DWORD dwBytesReturned;

    if (DllNtDll.pNtQueryInformationProcess == NULL) {
        return FALSE;
    }

    TargetProcess32BitPeb = YoriLibDoesProcessHave32BitPeb(hProcess);

    Status = DllNtDll.pNtQueryInformationProcess(hProcess, 0, &BasicInfo, sizeof(BasicInfo), &dwBytesReturned);
    if (Status != 0) {
        return FALSE;
    }

#if SETVER_DEBUG
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Peb at %p, Target %i bit PEB\n"), BasicInfo.PebBaseAddress, TargetProcess32BitPeb?32:64);
#endif

    if (TargetProcess32BitPeb) {
        YORI_LIB_PEB32_NATIVE ProcessPeb;

        if (!ReadProcessMemory(hProcess, BasicInfo.PebBaseAddress, &ProcessPeb, sizeof(ProcessPeb), &BytesReturned)) {
            return FALSE;
        }

        ProcessPeb.OSMajorVersion = Context->AppVerMajor;
        ProcessPeb.OSMinorVersion = Context->AppVerMinor;
        ProcessPeb.OSBuildNumber = (WORD)Context->AppBuildNumber;

        if (!WriteProcessMemory(hProcess, BasicInfo.PebBaseAddress, &ProcessPeb, sizeof(ProcessPeb), &BytesReturned)) {
            return FALSE;
        }
    } else {
        YORI_LIB_PEB64 ProcessPeb;

        if (!ReadProcessMemory(hProcess, BasicInfo.PebBaseAddress, &ProcessPeb, sizeof(ProcessPeb), &BytesReturned)) {
            return FALSE;
        }

        ProcessPeb.OSMajorVersion = Context->AppVerMajor;
        ProcessPeb.OSMinorVersion = Context->AppVerMinor;
        ProcessPeb.OSBuildNumber = (WORD)Context->AppBuildNumber;

        if (!WriteProcessMemory(hProcess, BasicInfo.PebBaseAddress, &ProcessPeb, sizeof(ProcessPeb), &BytesReturned)) {
            return FALSE;
        }
    }

    return TRUE;
}

/**
 Structure to query from NtQueryInformationThread since it is not defined
 by the compilation environment.
 */
typedef struct _YORI_THREAD_BASIC_INFORMATION {

    /**
     If the thread has terminated, the exit code for the thread.
     */
    LONG ExitStatus;

    /**
     Pointer to the native TEB for the thread.
     */
    PVOID TebAddress;

    /**
     A unique process handle.
     */
    HANDLE ProcessHandle;

    /**
     A unique thread handle.
     */
    HANDLE ThreadHandle;

    /**
     The processors the thread should be scheduled on.
     */
    ULONG_PTR AffinityMask;

    /**
     The priority of the thread.
     */
    LONG Priority;

    /**
     The base priority of the thread.
     */
    LONG BasePriority;
} YORI_THREAD_BASIC_INFORMATION, *PYORI_THREAD_BASIC_INFORMATION;

#if defined(_WIN64)

/**
 Apply the requested OS version into the 32 bit PEB of a WOW process.

 @param Context Pointer to the context describing the OS version to apply.

 @param hProcess Handle to the process to apply the OS version into.

 @param hThread Handle to a thread in the process.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SetVerApplyVersionToProcessWow(
    __in PSETVER_CONTEXT Context,
    __in HANDLE hProcess,
    __in HANDLE hThread
    )
{
    LONG Status;
    YORI_THREAD_BASIC_INFORMATION BasicInfo;
    SIZE_T BytesReturned;
    DWORD dwBytesReturned;
    PVOID Teb64Address;
    PVOID Teb32Address;
    PVOID Peb32Address;
    YORI_LIB_TEB32 Teb32;
    YORI_LIB_PEB32_WOW ProcessPeb;

    if (DllNtDll.pNtQueryInformationThread == NULL) {
        return FALSE;
    }

    Status = DllNtDll.pNtQueryInformationThread(hThread, 0, &BasicInfo, sizeof(BasicInfo), &dwBytesReturned);
    if (Status != 0) {
        return FALSE;
    }

    Teb64Address = BasicInfo.TebAddress;
    Teb32Address = (PUCHAR)Teb64Address + 0x2000;

#if SETVER_DEBUG
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("64 bit Teb at %p, 32 bit Teb at %p\n"), Teb64Address, Teb32Address);
#endif

    if (!ReadProcessMemory(hProcess, Teb32Address, &Teb32, sizeof(Teb32), &BytesReturned)) {
        return FALSE;
    }

    Peb32Address = (PVOID)(DWORD_PTR)Teb32.Peb32Address;

#if SETVER_DEBUG
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("32 bit Peb at %p\n"), Peb32Address);
#endif

    if (!ReadProcessMemory(hProcess, Peb32Address, &ProcessPeb, sizeof(ProcessPeb), &BytesReturned)) {
        return FALSE;
    }

    ProcessPeb.OSMajorVersion = Context->AppVerMajor;
    ProcessPeb.OSMinorVersion = Context->AppVerMinor;
    ProcessPeb.OSBuildNumber = (WORD)Context->AppBuildNumber;

    if (!WriteProcessMemory(hProcess, Peb32Address, &ProcessPeb, sizeof(ProcessPeb), &BytesReturned)) {
        return FALSE;
    }

    return TRUE;
}

#endif

#pragma warning(disable: 4152)
#pragma warning(disable: 4054)

/**
 Information about a process where the mini-debugger has observed it be
 launched and has not yet observed termination.  This is used so that the
 version information can be fixed up in all child processes once they
 reach their first instruction to execute.
 */
typedef struct _SETVER_OUTSTANDING_PROCESS {

    /**
     The linkage of this process within the list of known processes.
     */
    YORI_LIST_ENTRY ListEntry;

    /**
     A handle to the process.  This has been duplicated within this program
     and must be closed when freeing this structure.
     */
    HANDLE hProcess;

    /**
     A handle to the initial thread within the process.  This has been
     duplicated within this program and must be closed when freeing this
     structure.
     */
    HANDLE hInitialThread;

    /**
     Pointer within the target process' VA for where execution will commence.
     */
    LPTHREAD_START_ROUTINE StartRoutine;

    /**
     The process identifier for this process.
     */
    DWORD dwProcessId;

    /**
     The identifier for the initial thread within the process.
     */
    DWORD dwInitialThreadId;

    /**
     A saved copy of the first byte in the StartRoutine.  This may be an
     entire instruction or may be part of one, but since the breakpoint is
     a single byte, this byte must be restored before execution can
     resume.
     */
    UCHAR FirstInstruction;

    /**
     TRUE once the StartRoutine function has executed, indicating that the
     breakpoint has fired and been cleared, and no handling should be
     performed for exceptions from the StartRoutine again.
     */
    BOOLEAN ProcessStarted;
} SETVER_OUTSTANDING_PROCESS, *PSETVER_OUTSTANDING_PROCESS;

/**
 Find a process in the list of known processes by its process ID.

 @param ListHead Pointer to the list of known processes.

 @param dwProcessId The process identifier of the process whose information is
        requested.

 @return Pointer to the information block about the child process.
 */
PSETVER_OUTSTANDING_PROCESS
SetVerFindProcess(
    __in PYORI_LIST_ENTRY ListHead,
    __in DWORD dwProcessId
    )
{
    PYORI_LIST_ENTRY ListEntry;
    PSETVER_OUTSTANDING_PROCESS Process;

    ListEntry = NULL;
    while (TRUE) {
        ListEntry = YoriLibGetNextListEntry(ListHead, ListEntry);
        if (ListEntry == NULL) {
            break;
        }

        Process = CONTAINING_RECORD(ListEntry, SETVER_OUTSTANDING_PROCESS, ListEntry);
        if (Process->dwProcessId == dwProcessId) {
            return Process;
        }
    }

    return NULL;
}

/**
 Deallocate information about a single known child processes.

 @param Process Pointer to information about a single child process.
 */
VOID
SetVerFreeProcess(
    __in PSETVER_OUTSTANDING_PROCESS Process
    )
{
    YoriLibRemoveListItem(&Process->ListEntry);
    CloseHandle(Process->hProcess);
    CloseHandle(Process->hInitialThread);
    YoriLibFree(Process);
}

/**
 Deallocate information about all known child processes.

 @param ListHead Pointer to the list of known child processes.
 */
VOID
SetVerFreeAllProcesses(
    __in PYORI_LIST_ENTRY ListHead
    )
{
    PYORI_LIST_ENTRY ListEntry;
    PSETVER_OUTSTANDING_PROCESS Process;

    ListEntry = NULL;
    while (TRUE) {
        ListEntry = YoriLibGetNextListEntry(ListHead, ListEntry);
        if (ListEntry == NULL) {
            break;
        }

        Process = CONTAINING_RECORD(ListEntry, SETVER_OUTSTANDING_PROCESS, ListEntry);
        SetVerFreeProcess(Process);
        ListEntry = NULL;
    }
}


/**
 Pump debug events for child processes and complete when the initial process
 has terminated.

 @param Context Pointer to context describing the operations to perform.

 @return TRUE to indicate successful termination of the initial child process,
         FALSE to indicate failure.
 */
BOOL
SetVerPumpDebugEvents(
    __in PSETVER_CONTEXT Context
    )
{
    YORI_LIST_ENTRY Processes;
    PSETVER_OUTSTANDING_PROCESS Process;
    BOOL UseProcessBreakpoint;
    BOOL ProcessIsWow;
#if defined(_M_AMD64) || defined(_M_IX86)
    SIZE_T BytesWritten;
#endif

    YoriLibInitializeListHead(&Processes);

    while(TRUE) {
        DEBUG_EVENT DbgEvent;
        DWORD dwContinueStatus;

        ZeroMemory(&DbgEvent, sizeof(DbgEvent));
        if (!WaitForDebugEvent(&DbgEvent, INFINITE)) {
            break;
        }

#if SETVER_DEBUG
        YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("DbgEvent Pid %x Tid %x Event %x\n"), DbgEvent.dwProcessId, DbgEvent.dwThreadId, DbgEvent.dwDebugEventCode);
#endif

        dwContinueStatus = DBG_CONTINUE;

        switch(DbgEvent.dwDebugEventCode) {
            case CREATE_PROCESS_DEBUG_EVENT:

                Process = YoriLibMalloc(sizeof(SETVER_OUTSTANDING_PROCESS));
                if (Process == NULL) {
                    break;
                }

                ZeroMemory(Process, sizeof(SETVER_OUTSTANDING_PROCESS));
                DuplicateHandle(GetCurrentProcess(), DbgEvent.u.CreateProcessInfo.hProcess, GetCurrentProcess(), &Process->hProcess, 0, FALSE, DUPLICATE_SAME_ACCESS);
                DuplicateHandle(GetCurrentProcess(), DbgEvent.u.CreateProcessInfo.hThread, GetCurrentProcess(), &Process->hInitialThread, 0, FALSE, DUPLICATE_SAME_ACCESS);
                Process->StartRoutine = DbgEvent.u.CreateProcessInfo.lpStartAddress;
                Process->dwProcessId = DbgEvent.dwProcessId;
                Process->dwInitialThreadId = DbgEvent.dwThreadId;

#if SETVER_DEBUG
                YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("StartRoutine of Pid %x at %p\n"), DbgEvent.dwProcessId, Process->StartRoutine);
#endif

                //
                //  On x86/amd64, set a breakpoint at the entrypoint code
                //  and resume execution.  We'll fix up the versions over
                //  there.  On any other architecture, fix up the versions
                //  now and hope things go well.  We can't do this if the
                //  OS doesn't have Wow64GetThreadContext and the child
                //  process is 32 bit, since we have no way to process the
                //  breakpoints in the child.
                //

#if defined(_M_AMD64) || defined(_M_IX86)
                UseProcessBreakpoint = TRUE;
#else
                UseProcessBreakpoint = FALSE;
#endif
                ProcessIsWow = FALSE;
                if (DllKernel32.pIsWow64Process != NULL) {
                    if (DllKernel32.pIsWow64Process(Process->hProcess, &ProcessIsWow) && ProcessIsWow) {
                        if (DllKernel32.pWow64GetThreadContext == NULL ||
                            DllKernel32.pWow64SetThreadContext == NULL) {
                            UseProcessBreakpoint = FALSE;
                        }
                    }

                }

#if defined(_M_AMD64) || defined(_M_IX86)
                if (UseProcessBreakpoint) {
                    UCHAR BreakpointInstruction = 0xcc;
                    ReadProcessMemory(Process->hProcess, (LPVOID)Process->StartRoutine, &Process->FirstInstruction, sizeof(Process->FirstInstruction), &BytesWritten);
                    WriteProcessMemory(Process->hProcess, (LPVOID)Process->StartRoutine, &BreakpointInstruction, sizeof(BreakpointInstruction), &BytesWritten);
                    FlushInstructionCache(Process->hProcess, Process->StartRoutine, 1);
                }
#endif

                if (!UseProcessBreakpoint) {
                    if (!SetVerApplyVersionToProcess(Context, Process->hProcess)) {
                        break;
                    }
#if defined(_WIN64)
                    if (ProcessIsWow) {
                        if (!SetVerApplyVersionToProcessWow(Context, Process->hProcess, Process->hInitialThread)) {
                            break;
                        }
                    }
#endif
                    Process->ProcessStarted = TRUE;
                }

                YoriLibAppendList(&Processes, &Process->ListEntry);

                CloseHandle(DbgEvent.u.CreateProcessInfo.hFile);
                break;
            case EXIT_PROCESS_DEBUG_EVENT:
                Process = SetVerFindProcess(&Processes, DbgEvent.dwProcessId);
                ASSERT(Process != NULL);
                if (Process != NULL) {
                    SetVerFreeProcess(Process);
                }
                break;
            case LOAD_DLL_DEBUG_EVENT:
#if SETVER_DEBUG
                if (DbgEvent.u.LoadDll.lpImageName != NULL && DbgEvent.dwProcessId == Context->ProcessInfo->dwProcessId) {
                    SIZE_T BytesReturned;
                    PVOID DllNamePtr;
                    TCHAR DllName[128];

                    if (ReadProcessMemory(Context->ProcessInfo->hProcess, DbgEvent.u.LoadDll.lpImageName, &DllNamePtr, sizeof(DllNamePtr), &BytesReturned)) {
                        if (ReadProcessMemory(Context->ProcessInfo->hProcess, DllNamePtr, &DllName, sizeof(DllName), &BytesReturned)) {
                            if (DbgEvent.u.LoadDll.fUnicode) {
                                YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Dll loaded: %s\n"), DllName);
                            } else {
                                YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Dll loaded: %hs\n"), DllName);
                            }
                        }
                    }
                }
#endif
                CloseHandle(DbgEvent.u.LoadDll.hFile);
                break;
            case EXCEPTION_DEBUG_EVENT:

                //
                //  Wow64 processes throw a breakpoint once 32 bit code starts
                //  running, and the debugger is expected to handle it.  The
                //  two codes are for breakpoint and x86 breakpoint
                //

                dwContinueStatus = DBG_EXCEPTION_NOT_HANDLED;
                if (DbgEvent.u.Exception.ExceptionRecord.ExceptionCode == EXCEPTION_BREAKPOINT) {

                    dwContinueStatus = DBG_CONTINUE;

#if defined(_M_MRX000)
                    //
                    //  MIPS appears to continue from the instruction that
                    //  raised the exception.  We want to skip over it, and
                    //  fortunately we know all instructions are 4 bytes.
                    //  Note we only have a thread handle for the initial
                    //  thread, so we only process that; other threads will
                    //  crash the process.
                    //

                    Process = SetVerFindProcess(&Processes, DbgEvent.dwProcessId);
                    ASSERT(Process != NULL);
                    if (Process != NULL &&
                        Process->dwInitialThreadId == DbgEvent.dwThreadId) {

                        CONTEXT ThreadContext;
                        ZeroMemory(&ThreadContext, sizeof(ThreadContext));

                        ThreadContext.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER;
                        GetThreadContext(Process->hInitialThread, &ThreadContext);

                        ThreadContext.Fir += 4;
#if SETVER_DEBUG
                        YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Advancing Fir to %p\n"), ThreadContext.Fir);
#endif
                        SetThreadContext(Process->hInitialThread, &ThreadContext);
                    } else {
                        dwContinueStatus = DBG_EXCEPTION_NOT_HANDLED;
                    }
#elif defined(_M_AMD64) || defined(_M_IX86)
                    Process = SetVerFindProcess(&Processes, DbgEvent.dwProcessId);
                    ASSERT(Process != NULL);
                    if (Process != NULL &&
                        DbgEvent.u.Exception.ExceptionRecord.ExceptionAddress == Process->StartRoutine &&
                        !Process->ProcessStarted) {

                        CONTEXT ThreadContext;
                        ZeroMemory(&ThreadContext, sizeof(ThreadContext));

                        ThreadContext.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER;
                        GetThreadContext(Process->hInitialThread, &ThreadContext);
#if defined(_M_AMD64)
#if SETVER_DEBUG
                        YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("rip %p\n"), ThreadContext.Rip);
#endif
                        ThreadContext.Rip = (DWORD_PTR)(LPVOID)Process->StartRoutine;
#else
#if SETVER_DEBUG
                        YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("eip %p\n"), ThreadContext.Eip);
#endif
                        ThreadContext.Eip = (DWORD_PTR)(LPVOID)Process->StartRoutine;
#endif

                        WriteProcessMemory(Process->hProcess, (LPVOID)Process->StartRoutine, &Process->FirstInstruction, sizeof(Process->FirstInstruction), &BytesWritten);
                        FlushInstructionCache(Process->hProcess, Process->StartRoutine, 1);

                        SetThreadContext(Process->hInitialThread, &ThreadContext);
                        Process->ProcessStarted = TRUE;

                        if (!SetVerApplyVersionToProcess(Context, Process->hProcess)) {
                            break;
                        }
                    }
#endif
                }

                if (DbgEvent.u.Exception.ExceptionRecord.ExceptionCode == 0x4000001F) {

                    dwContinueStatus = DBG_CONTINUE;
#if defined(_M_AMD64)
                    Process = SetVerFindProcess(&Processes, DbgEvent.dwProcessId);
                    ASSERT(Process != NULL);
                    if (Process != NULL &&
                        DllKernel32.pWow64GetThreadContext != NULL &&
                        DllKernel32.pWow64SetThreadContext != NULL &&
                        DbgEvent.u.Exception.ExceptionRecord.ExceptionAddress == Process->StartRoutine &&
                        !Process->ProcessStarted) {

                        if (DbgEvent.u.Exception.ExceptionRecord.ExceptionAddress == Process->StartRoutine) {
                            YORI_LIB_WOW64_CONTEXT ThreadContext;
                            ZeroMemory(&ThreadContext, sizeof(ThreadContext));

                            ThreadContext.ContextFlags = YORI_WOW64_CONTEXT_CONTROL | YORI_WOW64_CONTEXT_INTEGER;
                            DllKernel32.pWow64GetThreadContext(Process->hInitialThread, &ThreadContext);
#if SETVER_DEBUG
                            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("eip %p\n"), ThreadContext.Eip);
#endif
                            ThreadContext.Eip = (DWORD)(DWORD_PTR)(LPVOID)Process->StartRoutine;
                            DllKernel32.pWow64SetThreadContext(Process->hInitialThread, &ThreadContext);
                            WriteProcessMemory(Process->hProcess, (LPVOID)Process->StartRoutine, &Process->FirstInstruction, sizeof(Process->FirstInstruction), &BytesWritten);
                            FlushInstructionCache(Process->hProcess, Process->StartRoutine, 1);
                            Process->ProcessStarted = TRUE;
                        }

                        if (!SetVerApplyVersionToProcess(Context, Process->hProcess)) {
                            break;
                        }
                        if (!SetVerApplyVersionToProcessWow(Context, Process->hProcess, Process->hInitialThread)) {
                            break;
                        }
                    }
#endif
                }


#if SETVER_DEBUG
                YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("ExceptionCode %x Address %p ContinueStatus %x\n"), DbgEvent.u.Exception.ExceptionRecord.ExceptionCode, DbgEvent.u.Exception.ExceptionRecord.ExceptionAddress, dwContinueStatus);
#endif

                break;
        }

        ContinueDebugEvent(DbgEvent.dwProcessId, DbgEvent.dwThreadId, dwContinueStatus);
        if (DbgEvent.dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT &&
            DbgEvent.dwProcessId == Context->ProcessInfo->dwProcessId) {
            break;
        }
    }

    SetVerFreeAllProcesses(&Processes);

    return TRUE;
}

#ifdef YORI_BUILTIN
/**
 The main entrypoint for the setver builtin command.
 */
#define ENTRYPOINT YoriCmd_SETVER
#else
/**
 The main entrypoint for the setver standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the setver cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the child process on success, or failure if the child
         could not be launched.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    YORI_STRING CmdLine;
    YORI_STRING Executable;
    PYORI_STRING ChildArgs;
    PROCESS_INFORMATION ProcessInfo;
    STARTUPINFO StartupInfo;
    DWORD ExitCode;
    BOOL ArgumentUnderstood;
    DWORD StartArg = 0;
    DWORD AppArg;
    DWORD i;
    YORI_STRING Arg;
    YORI_STRING WinVer;
    LONGLONG llTemp;
    DWORD CharsConsumed;
    SETVER_CONTEXT Context;

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                SetVerHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2018-2019"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                StartArg = i + 1;
                ArgumentUnderstood = TRUE;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    if (StartArg == 0 || StartArg == ArgC) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("setver: missing argument\n"));
        return EXIT_FAILURE;
    }

    memcpy(&WinVer, &ArgV[StartArg], sizeof(YORI_STRING));

    AppArg = StartArg + 1;
    if (AppArg >= ArgC) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("setver: missing argument\n"));
        return EXIT_FAILURE;
    }

    Context.AppVerMajor = 6;
    Context.AppVerMinor = 1;
    Context.AppBuildNumber = 7600;

    if (YoriLibStringToNumber(&WinVer, FALSE, &llTemp, &CharsConsumed)) {
        Context.AppVerMajor = (DWORD)llTemp;
        WinVer.LengthInChars -= CharsConsumed;
        WinVer.StartOfString += CharsConsumed;
        if (WinVer.LengthInChars > 0) {
            WinVer.LengthInChars -= 1;
            WinVer.StartOfString += 1;
            if (YoriLibStringToNumber(&WinVer, FALSE, &llTemp, &CharsConsumed)) {
                Context.AppVerMinor = (DWORD)llTemp;
                WinVer.LengthInChars -= CharsConsumed;
                WinVer.StartOfString += CharsConsumed;
                if (WinVer.LengthInChars > 0) {
                    WinVer.LengthInChars -= 1;
                    WinVer.StartOfString += 1;
                    if (YoriLibStringToNumber(&WinVer, FALSE, &llTemp, &CharsConsumed)) {
                        Context.AppBuildNumber = (DWORD)llTemp;
                    }
                }
            }
        }
    }

    YoriLibInitEmptyString(&Executable);
    if (!YoriLibLocateExecutableInPath(&ArgV[AppArg], NULL, NULL, &Executable)) {
        YoriLibInitEmptyString(&Executable);
    }

    if (Executable.LengthInChars == 0) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("setver: unable to find executable\n"));
        YoriLibFreeStringContents(&Executable);
        return EXIT_FAILURE;
    }

    ChildArgs = YoriLibMalloc((ArgC - AppArg) * sizeof(YORI_STRING));
    if (ChildArgs == NULL) {
        YoriLibFreeStringContents(&Executable);
        return EXIT_FAILURE;
    }

    memcpy(&ChildArgs[0], &Executable, sizeof(YORI_STRING));
    if (AppArg + 1 < ArgC) {
        memcpy(&ChildArgs[1], &ArgV[AppArg + 1], (ArgC - AppArg - 1) * sizeof(YORI_STRING));
    }

    if (!YoriLibBuildCmdlineFromArgcArgv(ArgC - AppArg, ChildArgs, TRUE, TRUE, &CmdLine)) {
        YoriLibFreeStringContents(&Executable);
        YoriLibFree(ChildArgs);
        return EXIT_FAILURE;
    }

    ASSERT(YoriLibIsStringNullTerminated(&CmdLine));

    memset(&StartupInfo, 0, sizeof(StartupInfo));
    StartupInfo.cb = sizeof(StartupInfo);

    if (!CreateProcess(NULL, CmdLine.StartOfString, NULL, NULL, TRUE, DEBUG_PROCESS | CREATE_DEFAULT_ERROR_MODE, NULL, NULL, &StartupInfo, &ProcessInfo)) {
        DWORD LastError = GetLastError();
        LPTSTR ErrText = YoriLibGetWinErrorText(LastError);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("setver: execution failed: %s"), ErrText);
        YoriLibFreeWinErrorText(ErrText);
        YoriLibFreeStringContents(&Executable);
        YoriLibFreeStringContents(&CmdLine);
        YoriLibFree(ChildArgs);
        return EXIT_FAILURE;
    }

    YoriLibFreeStringContents(&Executable);
    YoriLibFreeStringContents(&CmdLine);
    YoriLibFree(ChildArgs);

    Context.ProcessInfo = &ProcessInfo;

    if (!SetVerPumpDebugEvents(&Context)) {
        return EXIT_FAILURE;
    }

    WaitForSingleObject(ProcessInfo.hProcess, INFINITE);
    GetExitCodeProcess(ProcessInfo.hProcess, &ExitCode);
    CloseHandle(ProcessInfo.hProcess);
    CloseHandle(ProcessInfo.hThread);

    return ExitCode;
}

// vim:sw=4:ts=4:et:


<|start_filename|>wininfo/wininfo.c<|end_filename|>
/**
 * @file wininfo/wininfo.c
 *
 * Yori shell reposition or resize windows
 *
 * Copyright (c) 2018 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 Help text to display to the user.
 */
const
CHAR strWinInfoHelpText[] =
        "\n"
        "Return information about a window.\n"
        "\n"
        "WININFO [-license] [-f <fmt>] [-t <title>]\n"
        "\n"
        "Format specifiers are:\n"
        "   $left$         The offset from the left of the screen to the window\n"
        "   $top$          The offset from the top of the screen to the window\n"
        "   $width$        The width of the window\n"
        "   $height$       The height of the window\n";

/**
 Display usage text to the user.
 */
BOOL
WinInfoHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("WinInfo %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strWinInfoHelpText);
    return TRUE;
}

/**
 Context information used when populating the output string.
 */
typedef struct _WININFO_CONTEXT {

    /**
     Handle to the window to return information for.
     */
    HWND Window;

    /**
     The coordinates of the window.
     */
    RECT WindowRect;
} WININFO_CONTEXT, *PWININFO_CONTEXT;

/**
 A callback function to expand any known variables found when parsing the
 format string.

 @param OutputString A pointer to the output string to populate with data
        if a known variable is found.  The length allocated contains the
        length that can be populated with data.

 @param VariableName The variable name to expand.

 @param Context Pointer to a @ref WININFO_CONTEXT structure containing
        the data to populate.
 
 @return The number of characters successfully populated, or the number of
         characters required in order to successfully populate, or zero
         on error.
 */
DWORD
WinInfoExpandVariables(
    __inout PYORI_STRING OutputString,
    __in PYORI_STRING VariableName,
    __in PVOID Context
    )
{
    DWORD CharsNeeded;
    PWININFO_CONTEXT WinInfoContext = (PWININFO_CONTEXT)Context;

    if (YoriLibCompareStringWithLiteral(VariableName, _T("left")) == 0) {
        CharsNeeded = YoriLibSPrintfSize(_T("%i"), WinInfoContext->WindowRect.left);
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("top")) == 0) {
        CharsNeeded = YoriLibSPrintfSize(_T("%i"), WinInfoContext->WindowRect.top);
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("width")) == 0) {
        CharsNeeded = YoriLibSPrintfSize(_T("%i"), WinInfoContext->WindowRect.right - WinInfoContext->WindowRect.left);
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("height")) == 0) {
        CharsNeeded = YoriLibSPrintfSize(_T("%i"), WinInfoContext->WindowRect.bottom - WinInfoContext->WindowRect.top);
    } else {
        return 0;
    }

    if (OutputString->LengthAllocated < CharsNeeded) {
        return CharsNeeded;
    }

    if (YoriLibCompareStringWithLiteral(VariableName, _T("left")) == 0) {
        CharsNeeded = YoriLibSPrintf(OutputString->StartOfString, _T("%i"), WinInfoContext->WindowRect.left);
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("top")) == 0) {
        CharsNeeded = YoriLibSPrintf(OutputString->StartOfString, _T("%i"), WinInfoContext->WindowRect.top);
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("width")) == 0) {
        CharsNeeded = YoriLibSPrintf(OutputString->StartOfString, _T("%i"), WinInfoContext->WindowRect.right - WinInfoContext->WindowRect.left);
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("height")) == 0) {
        CharsNeeded = YoriLibSPrintf(OutputString->StartOfString, _T("%i"), WinInfoContext->WindowRect.bottom - WinInfoContext->WindowRect.top);
    }

    OutputString->LengthInChars = CharsNeeded;
    return CharsNeeded;
}

#ifdef YORI_BUILTIN
/**
 The main entrypoint for the wininfo builtin command.
 */
#define ENTRYPOINT YoriCmd_WININFO
#else
/**
 The main entrypoint for the wininfo standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the wininfo cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the child process on success, or failure if the child
         could not be launched.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD StartArg = 0;
    DWORD i;
    YORI_STRING Arg;
    PYORI_STRING WindowTitle = NULL;
    YORI_STRING DisplayString;
    WININFO_CONTEXT Context;
    LPTSTR FormatString = _T("Position: $left$*$top$\n")
                          _T("Size:     $width$*$height$\n");
    YORI_STRING YsFormatString;

    YoriLibInitEmptyString(&YsFormatString);
    ZeroMemory(&Context, sizeof(Context));

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                WinInfoHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2018"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("f")) == 0) {
                if (ArgC >= i + 1) {
                    YsFormatString.StartOfString = ArgV[i + 1].StartOfString;
                    YsFormatString.LengthInChars = ArgV[i + 1].LengthInChars;
                    ArgumentUnderstood = TRUE;
                    i++;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("t")) == 0) {
                if (ArgC >= i + 1) {
                    WindowTitle = &ArgV[i + 1];
                    ArgumentUnderstood = TRUE;
                    i++;
                }
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    YoriLibLoadUser32Functions();

    if (WindowTitle != NULL) {
        if (DllUser32.pFindWindowW == NULL) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("wininfo: operating system support not present\n"));
            return EXIT_FAILURE;
        }

        Context.Window = DllUser32.pFindWindowW(NULL, WindowTitle->StartOfString);
        if (Context.Window == NULL) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("wininfo: window not found\n"));
            return EXIT_FAILURE;
        }
        if (DllUser32.pGetWindowRect != NULL) {
            DllUser32.pGetWindowRect(Context.Window, &Context.WindowRect);
        }
    } else {
        if (DllUser32.pGetDesktopWindow == NULL) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("wininfo: operating system support not present\n"));
            return EXIT_FAILURE;
        }

        Context.Window = DllUser32.pGetDesktopWindow();
        if (DllUser32.pGetClientRect != NULL) {
            DllUser32.pGetClientRect(Context.Window, &Context.WindowRect);
        }
    }


    YoriLibInitEmptyString(&DisplayString);
    if (YsFormatString.StartOfString == NULL) {
        YoriLibConstantString(&YsFormatString, FormatString);
    }
    YoriLibExpandCommandVariables(&YsFormatString, '$', FALSE, WinInfoExpandVariables, &Context, &DisplayString);
    if (DisplayString.StartOfString != NULL) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%y"), &DisplayString);
        YoriLibFreeStringContents(&DisplayString);
    }

    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>sdir/display.c<|end_filename|>
/**
 * @file sdir/display.c
 *
 * Colorful, sorted and optionally rich directory enumeration
 * for Windows.
 *
 * This module implements low level routines to display formatted text to the
 * console or to files, in unicode or ansi as desired, pausing if too much
 * output has occurred, etc.
 *
 * Copyright (c) 2014-2018 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "sdir.h"

//
//  Display support
//

/**
 Write a specified number of characters to the output device.

 @param hConsole Handle to the output device.

 @param OutputString An array of characters to write to the output device.

 @param Length The number of characters in the array to write.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SdirWriteRawStringToOutputDevice(
    __in HANDLE hConsole,
    __in LPCTSTR OutputString,
    __in DWORD Length
    )
{
    YORI_STRING String;
    YoriLibInitEmptyString(&String);
    String.StartOfString = (LPTSTR)OutputString;
    String.LengthInChars = Length;

    return YoriLibOutputString(hConsole, 0, &String);
}

/**
 The most recently written attribute to the output.  This is used to ensure
 we only sent VT escapes to a device when the color is actually changing.
 */
YORILIB_COLOR_ATTRIBUTES SdirCurrentAttribute;

/**
 Sent a VT escape sequence to the output device if the new color is
 different to the previous one.

 @param hConsole Handle to the output device.

 @param Attribute The new color to set.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SdirSetConsoleTextAttribute(
    __in HANDLE hConsole,
    __in YORILIB_COLOR_ATTRIBUTES Attribute
    )
{
    if (YoriLibAreColorsIdentical(Attribute, SdirCurrentAttribute)) {
        return TRUE;
    }

    SdirCurrentAttribute.Ctrl = Attribute.Ctrl;
    SdirCurrentAttribute.Win32Attr = Attribute.Win32Attr;

    return YoriLibVtSetConsoleTextAttributeOnDevice(hConsole, 0, Attribute.Ctrl, Attribute.Win32Attr);
}

/**
 Write a string of characters with color attribute information to the current
 output device.

 @param str Pointer to the string of characters to write.

 @param count Number of characters to write.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SdirWrite (
    __in_ecount(count) PSDIR_FMTCHAR str,
    __in DWORD count
    )
{
    HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
    DWORD i, j;

    YORILIB_COLOR_ATTRIBUTES CacheAttr;
    TCHAR CharCache[64];

    CacheAttr.Ctrl = SdirDefaultColor.Ctrl;
    CacheAttr.Win32Attr = SdirDefaultColor.Win32Attr;

    //
    //  We firstly load as much as we can into a stack buffer, then write it
    //  out.  Those syscalls are expensive - run in a slow VM to see.  We
    //  have to write out the buffer either when the next character has 
    //  different color (so we can't use the same WriteConsole call) or when
    //  the stack buffer is full.
    //

    j = 0;
    for (i = 0; i < count; i++) { 

        CacheAttr.Ctrl = str[i].Attr.Ctrl;
        CacheAttr.Win32Attr = str[i].Attr.Win32Attr;
        CharCache[j] = str[i].Char;
        j++;

        if (( i + 1 < count && !YoriLibAreColorsIdentical(str[i + 1].Attr, CacheAttr)) || (j >= sizeof(CharCache)/sizeof(CharCache[0]))) {

            SdirSetConsoleTextAttribute(hConsole, CacheAttr);
            SdirWriteRawStringToOutputDevice(hConsole, CharCache, j);
            j = 0;
        }
    }

    //
    //  If we have anything left, flush it now.
    //

    if (j > 0) {

        SdirSetConsoleTextAttribute(hConsole, CacheAttr);
        SdirWriteRawStringToOutputDevice(hConsole, CharCache, j);
    }

    return TRUE;
}

/**
 Count of the number of lines written by this application.  Used to determine
 when "Press any key" should be displayed.
 */
DWORD SdirWriteStringLinesDisplayed = 0;

/**
 Write a NULL terminated string with a single color attribute to the output
 device.

 @param str Pointer to the NULL terminated string.

 @param DefaultAttribute The character color attribute to display.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SdirWriteStringWithAttribute (
    __in LPCTSTR str,
    __in YORILIB_COLOR_ATTRIBUTES DefaultAttribute
    )
{
    HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
    DWORD LinesInBuffer = 0;
    DWORD TCharsInBuffer = 0;
    DWORD OffsetOfLastLineBreak;
    BOOLEAN HitLineLimit = FALSE;
    CONSOLE_SCREEN_BUFFER_INFO ScreenInfo;

    SdirSetConsoleTextAttribute(hConsole, DefaultAttribute);

    if (Opts->OutputHasAutoLineWrap) {

        GetConsoleScreenBufferInfo(hConsole, &ScreenInfo);

        while (str[TCharsInBuffer] != '\0') {
            OffsetOfLastLineBreak = 0;
            while (str[TCharsInBuffer] != '\0') {
                if (str[TCharsInBuffer] == '\n') {
                    LinesInBuffer++;
                    OffsetOfLastLineBreak = TCharsInBuffer;
                } else if (TCharsInBuffer + ScreenInfo.dwCursorPosition.X - OffsetOfLastLineBreak >= Opts->ConsoleBufferWidth) {
                    LinesInBuffer++;
                    ScreenInfo.dwCursorPosition.X = 0;
                    OffsetOfLastLineBreak = TCharsInBuffer;
                }
                TCharsInBuffer++;
                if (SdirWriteStringLinesDisplayed + LinesInBuffer >= Opts->ConsoleHeight - 1) {
                    HitLineLimit = TRUE;
                    break;
                }
            }

            SdirWriteStringLinesDisplayed += LinesInBuffer;

            SdirWriteRawStringToOutputDevice(hConsole, str, TCharsInBuffer);

            LinesInBuffer = 0;
            str += TCharsInBuffer;
            TCharsInBuffer = 0;

            //
            //  This is screwy because we call it and it calls back into us.
            //  It works because we've just reset lines displayed to zero
            //  and we "know" we have enough space to display it.  Just in
            //  case it uses different attributes, reset those now.
            //

            if (HitLineLimit) {
                SdirWriteStringLinesDisplayed = 0;
                if (Opts->EnablePause && !SdirPressAnyKey()) {
                    return FALSE;
                }
                SdirSetConsoleTextAttribute(hConsole, DefaultAttribute);
                HitLineLimit = FALSE;
            }
        }

    } else {

        SdirSetConsoleTextAttribute(hConsole, DefaultAttribute);
        SdirWriteRawStringToOutputDevice(hConsole, str, (DWORD)_tcslen(str));
    }

    return TRUE;
}

/**
 Indicate that a row of text has been displayed external to the display
 package to ensure that the count of rows displayed remains accurate, 
 and check if "Press any key" processing is needed.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SdirRowDisplayed(VOID)
{
    if (Opts->EnablePause) {
        SdirWriteStringLinesDisplayed++;

        if (SdirWriteStringLinesDisplayed >= Opts->ConsoleHeight - 1) {

            SdirWriteStringLinesDisplayed = 0;
            if (!SdirPressAnyKey()) {
                return FALSE;
            }
            SdirWriteStringLinesDisplayed = 0;
        }
    }
    return TRUE;
}

/**
 Copy an array of characters with color information into a format character
 array where each character is recorded with its color.  Ensure that the
 amount of characters written is equal to a certain value, so that if the
 source string is shorter the destination buffer is populated with a constant
 number of characters.

 @param str The format string to populate with characters specifying color
        information.

 @param src An array of characters to populate into the formatted string.

 @param attr The color information to write for each of these characters.

 @param count The number of characters to populate from src.

 @param padsize The number of characters to write to the destination.  If
        more than count, the target string is padded with spaces with the
        same color information.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SdirPasteStrAndPad (
    __out_ecount(padsize) PSDIR_FMTCHAR str,
    __in_ecount_opt(count) LPTSTR src,
    __in YORILIB_COLOR_ATTRIBUTES attr,
    __in DWORD count,
    __in DWORD padsize
    )
{
    ULONG i;

    __analysis_assume(count == 0 || src != NULL);

    for (i = 0; i < count && i < padsize; i++) {
        str[i].Char = src[i];
        str[i].Attr.Ctrl = attr.Ctrl;
        str[i].Attr.Win32Attr = attr.Win32Attr;
    }

    for (; i < padsize; i++) {
        str[i].Char = ' ';
        str[i].Attr.Ctrl = attr.Ctrl;
        str[i].Attr.Win32Attr = attr.Win32Attr;
    }

    return TRUE;
}


/**
 Display a Win32 error string with a prefix string describing the context of
 the error.

 @param ErrorCode The Win32 error code to display the error string for.

 @param Prefix A NULL terminated prefix string describing the context of the
        error.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SdirDisplayError (
    __in LONG ErrorCode,
    __in_opt LPCTSTR Prefix
    )
{
    LPTSTR OutputBuffer;

    if (FormatMessage(
            FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
            NULL,
            ErrorCode,
            MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
            (LPTSTR)&OutputBuffer,
            0,
            NULL)) {

        if (Prefix) {
            SdirWriteStringWithAttribute(Prefix, Opts->FtError.HighlightColor);
            SdirWriteStringWithAttribute(_T(": "), Opts->FtError.HighlightColor);
        }
        if (!SdirWriteStringWithAttribute(OutputBuffer, Opts->FtError.HighlightColor)) {
            LocalFree((PVOID)OutputBuffer);
            return FALSE;
        }
        LocalFree((PVOID)OutputBuffer);
    }
    return TRUE;
}

/**
 Display a Win32 error string with a prefix string describing the context of
 the error.

 @param ErrorCode The Win32 error code to display the error string for.

 @param YsPrefix A prefix string describing the context of the error.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SdirDisplayYsError (
    __in LONG ErrorCode,
    __in PYORI_STRING YsPrefix
    )
{
    LPTSTR OutputBuffer;
    YORI_STRING EntireMsg;
    BOOL RetVal = FALSE;

    if (FormatMessage(
            FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
            NULL,
            ErrorCode,
            MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
            (LPTSTR)&OutputBuffer,
            0,
            NULL)) {

        YoriLibInitEmptyString(&EntireMsg);
        YoriLibYPrintf(&EntireMsg, _T("%y: %s"), YsPrefix, OutputBuffer);
        if (EntireMsg.StartOfString != NULL) {
            SdirWriteStringWithAttribute(EntireMsg.StartOfString, Opts->FtError.HighlightColor);
            YoriLibFreeStringContents(&EntireMsg);
            RetVal = TRUE;
        }
        LocalFree((PVOID)OutputBuffer);
    }
    return RetVal;
}

/**
 Tell the user to press a key to continue outputting information, and wait
 for the resulting key press.

 @return TRUE to indicate the application should continue processing and
         generating more output, FALSE to indicate the application should
         terminate.
 */
BOOL
SdirPressAnyKey (VOID)
{
    HANDLE hConsole = GetStdHandle(STD_INPUT_HANDLE);
    INPUT_RECORD InputBuffer;
    DWORD NumRead;

    SdirWriteString(_T("Press any key to continue..."));

    //
    //  Loop throwing away events until we get a key pressed
    //

    while (TRUE) {

        if (!ReadConsoleInput(hConsole, &InputBuffer, 1, &NumRead)) {
            break;
        }

        if (NumRead == 0) {
            break;
        }

        if (InputBuffer.EventType == KEY_EVENT &&
            InputBuffer.Event.KeyEvent.bKeyDown &&
            InputBuffer.Event.KeyEvent.uChar.UnicodeChar != '\0') {

            if (InputBuffer.Event.KeyEvent.uChar.UnicodeChar == 'q' ||
                InputBuffer.Event.KeyEvent.uChar.UnicodeChar == 'Q') {

                return FALSE;
            }

            break;
        }
    }

    //
    //  Keep throwing away any remaining events so we're not left
    //  with stale events next time we run
    //

    while (PeekConsoleInput(hConsole, &InputBuffer, 1, &NumRead) &&
           NumRead > 0) {

        ReadConsoleInput(hConsole, &InputBuffer, 1, &NumRead);
    }

    SdirWriteString(_T("\n"));
    SdirWriteStringLinesDisplayed = 0;
    return TRUE;
}

// vim:sw=4:ts=4:et:


<|start_filename|>which/Makefile<|end_filename|>

BINARIES=which.exe

!INCLUDE "..\config\ver.mk"
YORI_BIN_VER_MAJOR=$(WHICH_VER_MAJOR)
YORI_BIN_VER_MINOR=$(WHICH_VER_MINOR)
!INCLUDE "..\config\common.mk"

!IF $(PDB)==1
LINKPDB=/Pdb:which.pdb
!ENDIF

CFLAGS=$(CFLAGS) -DWHICH_VER_MAJOR=$(YORI_BIN_VER_MAJOR) -DWHICH_VER_MINOR=$(YORI_BIN_VER_MINOR)

BIN_OBJS=\
	 which.obj         \
	 resource.obj      \

MOD_OBJS=\
	 mod_which.obj     \

compile: $(BIN_OBJS) builtins.lib

which.exe: $(BIN_OBJS) $(YORILIB) $(YORICRT)
	@echo $@
	@$(LINK) $(LDFLAGS) -entry:$(YENTRY) $(BIN_OBJS) $(LIBS) $(CRTLIB) $(YORILIB) -version:$(YORI_BIN_VER_MAJOR).$(YORI_BIN_VER_MINOR) $(LINKPDB) -out:$@

mod_which.obj: which.c
	@echo $@
	@$(CC) -c -DYORI_BUILTIN=1 $(CFLAGS) -Fo$@ which.c

builtins.lib: $(MOD_OBJS)
	@echo $@
	@$(LIB32) $(LIBFLAGS) $(MOD_OBJS) -out:$@


<|start_filename|>lib/path.c<|end_filename|>
/**
 * @file lib/path.c
 *
 * Yori lookup expression in path and determine if it's an external executable
 *
 * Copyright (c) 2017 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "yoripch.h"
#include "yorilib.h"

/**
 Searches an environment variable with semicolon delimited elements for a file
 name match.

 @param FileName The file name to search for within the environment variable.

 @param EnvVarData The contents of the environment variable to search.

 @param ScratchArea A buffer for this function to use while probing for files.
        This must be OutLen chars in size.

 @param MatchAllCallback An optional callback to invoke each time a
        candidate match is found.

 @param MatchAllContext Context information to supply to MatchAllCallback
        if it is specified.

 @param Out On successful completion, a buffer to copy the resulting match.
        If no match is found, this is initialized as an empty string.

 @param FullPath If TRUE, the caller wants an absolute, escaped path name.
        If FALSE, a non-escaped absolute path name is desired.
 */
VOID
YoriLibSearchEnv(
    __in PYORI_STRING FileName,
    __in PYORI_STRING EnvVarData,
    __in PYORI_STRING ScratchArea,
    __in_opt PYORI_LIB_PATH_MATCH_FN MatchAllCallback,
    __in_opt PVOID MatchAllContext,
    __inout PYORI_STRING Out,
    __in BOOL FullPath
    )
{
    TCHAR CONST * begin;
    TCHAR CONST * end;
    TCHAR search;
    HANDLE hFind;
    WIN32_FIND_DATA FindData;
    LPTSTR fn;

    ASSERT(YoriLibIsStringNullTerminated(FileName));
    ASSERT(YoriLibIsStringNullTerminated(EnvVarData));

    //
    //  If we can't possibly do anything, stop.
    //

    if (Out->LengthAllocated == 0) {
        return;
    }

    //
    //  Check the current directory first
    //

    hFind = FindFirstFile(FileName->StartOfString, &FindData);

    if (hFind != INVALID_HANDLE_VALUE) {
        FindClose(hFind);
        if (!YoriLibGetFullPathNameReturnAllocation(FileName, FullPath, Out, &fn) || fn == NULL) {
            Out->LengthInChars = 0;
            Out->StartOfString[0] = '\0';
            return;
        }
        Out->LengthInChars -= Out->LengthInChars - (DWORD)(fn - Out->StartOfString);
        Out->LengthInChars += YoriLibSPrintfS(fn, Out->LengthAllocated - (DWORD)(fn-Out->StartOfString), _T("%s"), FindData.cFileName);
        if (MatchAllCallback != NULL) {
            if (!MatchAllCallback(Out, MatchAllContext)) {
                return;
            }
        } else {
            return;
        }
    }

    Out->StartOfString[0] = '\0';

    begin = EnvVarData->StartOfString;

    while (*begin == ';') begin++;

    while (*begin) {

        DWORD componentlen;

        search = ';';

        if (*begin == '"') {
            begin++;
            search = '"';
        }

        end = _tcschr(begin, search);

        if (end == NULL) {

            end = begin + _tcslen(begin);
        }

        componentlen = (DWORD)(end - begin);

        if (componentlen != 0 && componentlen + 1 + FileName->LengthInChars < Out->LengthAllocated) {

            memcpy(ScratchArea->StartOfString, (LPTSTR)begin, componentlen * sizeof(TCHAR));
            memcpy(ScratchArea->StartOfString + componentlen, _T("\\"), sizeof(TCHAR));
            YoriLibSPrintf(ScratchArea->StartOfString + componentlen + 1, _T("%y"), FileName);
            ScratchArea->LengthInChars = componentlen + 1 + FileName->LengthInChars;

            hFind = FindFirstFile(ScratchArea->StartOfString, &FindData);

            if (hFind != INVALID_HANDLE_VALUE) {
                FindClose(hFind);
                if (!YoriLibGetFullPathNameReturnAllocation(ScratchArea, FullPath, Out, &fn) || fn == NULL) {
                    Out->LengthInChars = 0;
                    Out->StartOfString[0] = '\0';
                    return;
                }
                Out->LengthInChars -= Out->LengthInChars - (DWORD)(fn - Out->StartOfString);
                Out->LengthInChars += YoriLibSPrintfS(fn, Out->LengthAllocated - (DWORD)(fn-Out->StartOfString), _T("%s"), FindData.cFileName);
                if (MatchAllCallback != NULL) {
                    if (!MatchAllCallback(Out, MatchAllContext)) {
                        return;
                    }
                } else {
                    return;
                }
            }
        }

        begin = end;
        if (search == '"') {
            while (*begin == '"') begin++;
        }

        while (*begin == ';') begin++;
    }
}

/**
 A decomposed form of the PathExt environment variable, containing an array
 of this structure, indicating the extension name as a counted string and
 a boolean flag to indicate whether or not a match was found.  This allows
 a searcher to enumerate all files marking what was found, then later checking
 which one should be "first."
 */
typedef struct _YORI_PATHEXT_COMPONENT {

    /**
     The extension to search for.
     */
    YORI_STRING Extension;

    /**
     Set to TRUE if the extension was found, remains FALSE if it was not.
     */
    BOOL Found;
} YORI_PATHEXT_COMPONENT, *PYORI_PATHEXT_COMPONENT;

/**
 Convert a directory name and matched file within that directory into a fully
 qualified file path.

 @param SearchPath Pointer to the directory being searched.

 @param Match Pointer to the object found within the directory.

 @param Out On successful completion, updated to point to a fully qualified
        path to the file.

 @param FullPath If TRUE, return an escaped form of the path; if FALSE, return
        a Win32 path without any escape.

 @return TRUE to indicate the lookup was successful, and FALSE to indicate a
         lookup failure.  Success does not imply a match was found; if a
         lookup successfully found nothing, FoundPath will contain an empty
         string.
 */
__success(return)
BOOL
YoriLibLocateBuildFullName(
    __in PYORI_STRING SearchPath,
    __in PWIN32_FIND_DATA Match,
    __inout PYORI_STRING Out,
    __in BOOL FullPath
    )
{

    YORI_STRING FoundPath;
    LPTSTR fn;
    BOOL NeedsSeperator;
    DWORD FileNameLen = _tcslen(Match->cFileName);

    if (!YoriLibAllocateString(&FoundPath, SearchPath->LengthInChars + 1 + FileNameLen + 1)) {
        return FALSE;
    }

    memcpy(FoundPath.StartOfString,
           SearchPath->StartOfString,
           SearchPath->LengthInChars * sizeof(TCHAR));

    //
    //  Normally we'd build the search path, a seperator, and the file
    //  criteria.  If the search path is just an X: prefix though, we
    //  really don't want to add the seperator, which would completely
    //  change the meaning of the request.
    //

    NeedsSeperator = TRUE;
    if (SearchPath->LengthInChars == 2 &&
        SearchPath->StartOfString[1] == ':') {

        NeedsSeperator = FALSE;
    }

    if (NeedsSeperator) {
        FoundPath.StartOfString[SearchPath->LengthInChars] = '\\';
        FoundPath.LengthInChars = SearchPath->LengthInChars + 1;
    } else {
        FoundPath.LengthInChars = SearchPath->LengthInChars;
    }

    memcpy(&FoundPath.StartOfString[FoundPath.LengthInChars], Match->cFileName, (FileNameLen + 1) * sizeof(TCHAR));
    FoundPath.LengthInChars += FileNameLen;

    if (!YoriLibGetFullPathNameReturnAllocation(&FoundPath, FullPath, Out, &fn)) {
        YoriLibFreeStringContents(&FoundPath);
        return FALSE;
    }
    YoriLibFreeStringContents(&FoundPath);

    return TRUE;
}

/**
 A hardcoded search order for file extensions if the environment variable is
 not defined.
 */
LPCTSTR YoriLibDefaultPathExt = _T(".com;.exe;.bat;.cmd");

/**
 Convert the string based pathext environment variable into an array of
 potential components that are stored in search order and can be marked
 if a match is found in any directory.  Because search order is not the
 same as enumerate order, we need a process to determine what is found
 and a seperate step for which is first.

 @param ComponentCount On successful completion, the number of elements
        stored in the path ext array.

 @return On successful completion, points to an array of pathext components.
         The caller is expected to free this with
         @ref YoriLibPathFreePathExtComponents .
 */
__success(return != NULL)
PYORI_PATHEXT_COMPONENT
YoriLibPathBuildPathExtComponentList(
    __out PDWORD ComponentCount
    )
{
    BOOLEAN UseDefaultPathExt = FALSE;
    TCHAR * PathExtString;
    DWORD PathExtLength;
    DWORD PathExtCount;
    DWORD PathExtIndex;
    TCHAR * ThisExt;
    TCHAR * TokCtx;
    PYORI_PATHEXT_COMPONENT PathExtComponents;

    //
    //  Check if the PathExt environment variable is defined, and if so,
    //  how big it is.  Allocate enough memory to capture it.  If it's
    //  not there, fall back to the default string.
    //

    UseDefaultPathExt = FALSE;
    PathExtLength = GetEnvironmentVariable(_T("PATHEXT"), NULL, 0);
    if (PathExtLength == 0) {
        PathExtLength = _tcslen(YoriLibDefaultPathExt) + sizeof(TCHAR);
        UseDefaultPathExt = TRUE;
    }

    PathExtString = YoriLibReferencedMalloc(PathExtLength * sizeof(TCHAR));
    if (PathExtString == NULL) {
        return NULL;
    }

    if (UseDefaultPathExt) {
        _tcscpy(PathExtString, YoriLibDefaultPathExt);
    } else {
        GetEnvironmentVariable(_T("PATHEXT"), PathExtString, PathExtLength);
    }

    //
    //  Count the number of elements in pathext
    //

    ThisExt = _tcstok_s(PathExtString, _T(";"), &TokCtx);
    PathExtCount = 0;
    while (ThisExt != NULL) {
        if (ThisExt[0] != '\0') {
            PathExtCount++;
        }
        ThisExt = _tcstok_s(NULL, _T(";"), &TokCtx);
    }

    //
    //  Allocate and populate an array of pathext values
    //

    PathExtComponents = YoriLibMalloc(sizeof(YORI_PATHEXT_COMPONENT) * PathExtCount);
    if (PathExtComponents == NULL) {
        YoriLibFree(PathExtString);
        return NULL;
    }

    if (UseDefaultPathExt) {
        _tcscpy(PathExtString, YoriLibDefaultPathExt);
    } else {
        GetEnvironmentVariable(_T("PATHEXT"), PathExtString, PathExtLength);
    }

    ThisExt = _tcstok_s(PathExtString, _T(";"), &TokCtx);
    PathExtIndex = 0;
    while (ThisExt != NULL) {
        if (ThisExt[0] != '\0') {
            YoriLibReference(PathExtString);
            PathExtComponents[PathExtIndex].Extension.MemoryToFree = PathExtString;
            PathExtComponents[PathExtIndex].Extension.StartOfString = ThisExt;
            PathExtComponents[PathExtIndex].Extension.LengthInChars = _tcslen(ThisExt);
            PathExtComponents[PathExtIndex].Extension.LengthAllocated = PathExtComponents[PathExtIndex].Extension.LengthInChars;
            PathExtIndex++;
        }
        ThisExt = _tcstok_s(NULL, _T(";"), &TokCtx);
    }

    YoriLibDereference(PathExtString);

    *ComponentCount = PathExtCount;
    return PathExtComponents;
}

/**
 Frees a path ext component array previously allocated with @ref
 YoriLibPathBuildPathExtComponentList .

 @param PathExtComponents Pointer to the array of path ext components,
        as returned by @ref YoriLibPathBuildPathExtComponentList .

 @param PathExtComponentCount The number of elements in PathExtComponents .
 */
VOID
YoriLibPathFreePathExtComponents(
    __in PYORI_PATHEXT_COMPONENT PathExtComponents,
    __in DWORD PathExtComponentCount
    )
{
    DWORD Count;

    for (Count = 0; Count < PathExtComponentCount; Count++) {
        if (PathExtComponents[Count].Extension.MemoryToFree != NULL) {
            YoriLibDereference(PathExtComponents[Count].Extension.MemoryToFree);
            PathExtComponents[Count].Extension.MemoryToFree = NULL;
        }
    }

    YoriLibFree(PathExtComponents);
}

/**
 Search through a single path matching against desired file extensions.

 @param FileName Pointer to the base file name to search for.

 @param SearchPath The directory to search through for matches.

 @param ScratchArea Pointer to a buffer that this routine can freely use.
        It is returned in allocated form to the caller so if this function
        is invoked multiple times the same allocation can be recycled.

 @param PathExtData Points to an array of file name extensions to search for.

 @param PathExtCount The number of elements in PathExtData.

 @param MatchAllCallback Optionally points to a callback function to be invoked
       on every potential match found.  If not specified, the first match is
       returned in Out.

 @param MatchAllContext Optionally points to context to supply to
        MatchAllCallback .

 @param Out Points to a buffer to populate with the first match if
        MatchAllCallback is not specified.

 @param FullPath If TRUE, return an escaped form of the path; if FALSE, return
        a Win32 path without any escape.

 @return TRUE to indicate the lookup was successful, and FALSE to indicate a
         lookup failure.  Success does not imply a match was found; if a
         lookup successfully found nothing, FoundPath will contain an empty
         string.
 */
__success(return)
BOOL
YoriLibLocateFileExtensionsInOnePath(
    __in PYORI_STRING FileName,
    __in PYORI_STRING SearchPath,
    __in PYORI_STRING ScratchArea,
    __inout PYORI_PATHEXT_COMPONENT PathExtData,
    __in DWORD PathExtCount,
    __in_opt PYORI_LIB_PATH_MATCH_FN MatchAllCallback,
    __in_opt PVOID MatchAllContext,
    __inout PYORI_STRING Out,
    __in BOOL FullPath
    )
{
    HANDLE hFind;
    DWORD FileNameLen;
    WIN32_FIND_DATA FindData;
    WIN32_FIND_DATA *PathExtMatches;
    YORI_STRING SearchName;
    BOOL PartialMatchOkay;
    BOOL NeedsSeperator;
    DWORD Count;

    //
    //  If we can't possibly do anything, stop.
    //

    if (Out->LengthAllocated == 0 || FileName->LengthInChars == 0) {
        return FALSE;
    }

    Out->StartOfString[0] = '\0';

    //
    //  If the caller has specified a file name with a wildcard, they're
    //  okay finding anything.  If not, we need to make sure the file
    //  name specified plus the extension we find is an exact match.
    //

    if (FileName->StartOfString[FileName->LengthInChars - 1] == '*') {
        PartialMatchOkay = TRUE;
    } else {
        PartialMatchOkay = FALSE;
    }

    //
    //  Allocate a scratch area for the directory, search file name, a
    //  seperator, a wildcard, and a terminator; as well as find results
    //  for each matching extension.  In case this function is invoked
    //  repeatedly, overallocate somewhat if we're forced to allocate
    //  here.
    //

    FileNameLen = SearchPath->LengthInChars + FileName->LengthInChars + 3;

    if (ScratchArea->LengthAllocated < (FileNameLen * sizeof(TCHAR) + sizeof(WIN32_FIND_DATA) * PathExtCount)) {
        YoriLibFreeStringContents(ScratchArea);
        if (!YoriLibAllocateString(ScratchArea, (FileNameLen + 0x40) * sizeof(TCHAR) + sizeof(WIN32_FIND_DATA) * PathExtCount)) {
            return FALSE;
        }
    }

    YoriLibInitEmptyString(&SearchName);
    SearchName.StartOfString = ScratchArea->StartOfString;
    SearchName.LengthAllocated = FileNameLen;

    PathExtMatches = (PWIN32_FIND_DATA)YoriLibAddToPointer(ScratchArea->StartOfString, SearchName.LengthAllocated * sizeof(TCHAR));

    //
    //  Normally we'd build the search path, a seperator, and the file
    //  criteria.  If the search path is just an X: prefix though, we
    //  really don't want to add the seperator, which would completely
    //  change the meaning of the request.  If the search path ends in
    //  a seperator already, don't add another.
    //

    NeedsSeperator = TRUE;
    if (SearchPath->LengthInChars == 2 &&
        SearchPath->StartOfString[1] == ':') {

        NeedsSeperator = FALSE;
    } else if (SearchPath->LengthInChars > 0 &&
               YoriLibIsSep(SearchPath->StartOfString[SearchPath->LengthInChars - 1])) {

        NeedsSeperator = FALSE;
    }

    //
    //  Populate the scratch area with the aforementioned directory,
    //  seperator, file name, wildcard, and terminator
    //

    memcpy(SearchName.StartOfString,
           SearchPath->StartOfString,
           SearchPath->LengthInChars * sizeof(TCHAR));

    if (NeedsSeperator) {
        SearchName.StartOfString[SearchPath->LengthInChars] = '\\';
        SearchName.LengthInChars = SearchPath->LengthInChars + 1;
    } else {
        SearchName.LengthInChars = SearchPath->LengthInChars;
    }

    memcpy(&SearchName.StartOfString[SearchName.LengthInChars],
           FileName->StartOfString,
           FileName->LengthInChars * sizeof(TCHAR));

    SearchName.StartOfString[SearchName.LengthInChars + FileName->LengthInChars] = '*';
    SearchName.StartOfString[SearchName.LengthInChars + FileName->LengthInChars + 1] = '\0';
    SearchName.LengthInChars += FileName->LengthInChars;
    ASSERT(SearchName.LengthInChars < SearchName.LengthAllocated);

    //
    //  Before we start searching, indicate that we haven't found anything.
    //

    for (Count = 0; Count < PathExtCount; Count++) {
        PathExtData[Count].Found = FALSE;
    }

    //
    //  Search the directory for all files with this prefix.
    //

    hFind = FindFirstFile(SearchName.StartOfString, &FindData);
    if (hFind == INVALID_HANDLE_VALUE) {
        return TRUE;
    }

    //
    //  For every file we find in the pathext list, mark it as found.
    //

    do {
        FileNameLen = _tcslen(FindData.cFileName);
        for (Count = 0; Count < PathExtCount; Count++) {
            if (!PathExtData[Count].Found &&
                FileNameLen > PathExtData[Count].Extension.LengthInChars) {

                if (_tcsnicmp(PathExtData[Count].Extension.StartOfString, &FindData.cFileName[FileNameLen - PathExtData[Count].Extension.LengthInChars], PathExtData[Count].Extension.LengthInChars) == 0) {

                    //
                    //  If we are looking for all matches of a partial match,
                    //  recurse looking for all extensions of a non-partial
                    //  match.  This may return duplicates; this logic currently
                    //  assumes the upper layer can handle that.
                    //

                    if (PartialMatchOkay && MatchAllCallback != NULL) {
                        PYORI_PATHEXT_COMPONENT ChildPathExtComponents;
                        DWORD ChildPathExtCount;
                        YORI_STRING ChildFileName;
                        YORI_STRING ChildScratchArea;

                        ChildPathExtComponents = YoriLibPathBuildPathExtComponentList(&ChildPathExtCount);
                        if (ChildPathExtComponents == NULL) {
                            FindClose(hFind);
                            return FALSE;
                        }

                        YoriLibInitEmptyString(&ChildScratchArea);
                        ChildFileName.StartOfString = FindData.cFileName;
                        ChildFileName.LengthInChars = FileNameLen - PathExtData[Count].Extension.LengthInChars;
                        ChildFileName.LengthAllocated = FileNameLen + 1;

                        if (!YoriLibLocateFileExtensionsInOnePath(&ChildFileName,
                                                                  SearchPath,
                                                                  &ChildScratchArea,
                                                                  ChildPathExtComponents,
                                                                  ChildPathExtCount,
                                                                  MatchAllCallback,
                                                                  MatchAllContext,
                                                                  Out,
                                                                  FullPath)) {

                            YoriLibPathFreePathExtComponents(ChildPathExtComponents, ChildPathExtCount);
                            YoriLibFreeStringContents(&ChildScratchArea);
                            FindClose(hFind);
                            return FALSE;
                        }

                        YoriLibFreeStringContents(&ChildScratchArea);
                        YoriLibPathFreePathExtComponents(ChildPathExtComponents, ChildPathExtCount);

                    } else if (FileNameLen - PathExtData[Count].Extension.LengthInChars == FileName->LengthInChars) {

                        PathExtData[Count].Found = TRUE;
                        memcpy(&PathExtMatches[Count], &FindData, sizeof(WIN32_FIND_DATA));
                    }
                }
            }
        }

    } while(FindNextFile(hFind, &FindData));

    FindClose(hFind);

    if (MatchAllCallback != NULL) {
        for (Count = 0; Count < PathExtCount; Count++) {
            if (PathExtData[Count].Found) {
                if (!YoriLibLocateBuildFullName(SearchPath,
                                                &PathExtMatches[Count],
                                                Out,
                                                FullPath)) {
                    return FALSE;
                }
                if (!MatchAllCallback(Out, MatchAllContext)) {
                    return FALSE;
                }
                Out->StartOfString[0] = '\0';
            }
        }
        Out->StartOfString[0] = '\0';
        return TRUE;
    }

    //
    //  Based on the initial search criteria, decide what if anything to
    //  return
    //

    for (Count = 0; Count < PathExtCount; Count++) {
        if (PathExtData[Count].Found) {

            if (!YoriLibLocateBuildFullName(SearchPath,
                                            &PathExtMatches[Count],
                                            Out,
                                            FullPath)) {
                return FALSE;
            }

            break;
        }
    }

    return TRUE;
}

/**
 Perform a path search for a file which could have any path and could have any
 extension.

 @param SearchFor The file name to search for.

 @param PathVariable The contents of the environment variable to search through.

 @param MatchAllCallback Optional callback to be invoked on every single match.
        If not specified, this function returns the first match.

 @param MatchAllContext Optional context to pass to the callback, if it is
        specified.

 @param FoundPath If not specified, this string is updated to contain any
        matching file.

 @return TRUE to indicate the lookup was successful, and FALSE to indicate a
         lookup failure.  Success does not imply a match was found; if a
         lookup successfully found nothing, FoundPath will contain an empty
         string.
 */
__success(return)
BOOL
YoriLibPathLocateUnknownExtensionUnknownLocation(
    __in PYORI_STRING SearchFor,
    __in PYORI_STRING PathVariable,
    __in_opt PYORI_LIB_PATH_MATCH_FN MatchAllCallback,
    __in_opt PVOID MatchAllContext,
    __inout PYORI_STRING FoundPath
    )
{
    TCHAR * TokCtx;
    PYORI_PATHEXT_COMPONENT PathExtComponents;
    DWORD PathExtCount;
    LPTSTR ThisPath;
    YORI_STRING SearchPath;
    YORI_STRING ScratchArea;

    ASSERT(YoriLibIsStringNullTerminated(PathVariable));

    PathExtComponents = YoriLibPathBuildPathExtComponentList(&PathExtCount);
    if (PathExtComponents == NULL) {
        return FALSE;
    }

    //
    //  MSFIX Should probably be quote aware
    //

    //
    //  First, check the current directory.
    //

    YoriLibInitEmptyString(&ScratchArea);
    YoriLibConstantString(&SearchPath, _T("."));
    FoundPath->StartOfString[0] = '\0';
    FoundPath->LengthInChars = 0;

    if (!YoriLibLocateFileExtensionsInOnePath(SearchFor,
                                              &SearchPath,
                                              &ScratchArea,
                                              PathExtComponents,
                                              PathExtCount,
                                              MatchAllCallback,
                                              MatchAllContext,
                                              FoundPath,
                                              FALSE)) {
        YoriLibFreeStringContents(&ScratchArea);
        YoriLibPathFreePathExtComponents(PathExtComponents, PathExtCount);
        return FALSE;
    }

    //
    //  If we don't have a match, check each of the path components
    //  until we find one.
    //

    if (FoundPath->StartOfString[0] == '\0') {
        ThisPath = _tcstok_s(PathVariable->StartOfString, _T(";"), &TokCtx);
        while (ThisPath != NULL) {

            if (ThisPath[0] != '\0') {
                SearchPath.MemoryToFree = NULL;
                SearchPath.StartOfString = ThisPath;
                SearchPath.LengthInChars = _tcslen(ThisPath);
                SearchPath.LengthAllocated = SearchPath.LengthInChars + 1;

                FoundPath->LengthInChars = 0;

                if (!YoriLibLocateFileExtensionsInOnePath(SearchFor,
                                                          &SearchPath,
                                                          &ScratchArea,
                                                          PathExtComponents,
                                                          PathExtCount,
                                                          MatchAllCallback,
                                                          MatchAllContext,
                                                          FoundPath,
                                                          FALSE)) {
                    YoriLibFreeStringContents(&ScratchArea);
                    YoriLibPathFreePathExtComponents(PathExtComponents, PathExtCount);
                    return FALSE;
                }

                if (FoundPath->StartOfString[0] != '\0') {
                    break;
                }
            }

            ThisPath = _tcstok_s(NULL, _T(";"), &TokCtx);
        }
    }

    YoriLibPathFreePathExtComponents(PathExtComponents, PathExtCount);
    YoriLibFreeStringContents(&ScratchArea);
    return TRUE;
}

/**
 Perform a path search for a file with a known path that could have any
 extension.

 @param SearchFor The file name to search for.

 @param MatchAllCallback Optional callback to be invoked on every single match.
        If not specified, this function returns the first match.

 @param MatchAllContext Optional context to pass to the callback, if it is
        specified.

 @param FoundPath If not specified, this string is updated to contain any
        matching file.

 @return TRUE to indicate the lookup was successful, and FALSE to indicate a
         lookup failure.  Success does not imply a match was found; if a
         lookup successfully found nothing, FoundPath will contain an empty
         string.
 */
__success(return)
BOOL
YoriLibPathLocateUnknownExtensionKnownLocation(
    __in PYORI_STRING SearchFor,
    __in_opt PYORI_LIB_PATH_MATCH_FN MatchAllCallback,
    __in_opt PVOID MatchAllContext,
    __inout PYORI_STRING FoundPath
    )
{
    PYORI_PATHEXT_COMPONENT PathExtComponents;
    DWORD PathExtCount;
    YORI_STRING FileNameToFind;
    YORI_STRING DirectoryToSearch;
    YORI_STRING ScratchArea;
    DWORD PathSeperator;

    PathExtComponents = YoriLibPathBuildPathExtComponentList(&PathExtCount);
    if (PathExtComponents == NULL) {
        return FALSE;
    }

    YoriLibInitEmptyString(&DirectoryToSearch);
    DirectoryToSearch.StartOfString = SearchFor->StartOfString;
    DirectoryToSearch.LengthInChars = SearchFor->LengthInChars;

    for (PathSeperator = DirectoryToSearch.LengthInChars - 1;
         TRUE;
         PathSeperator--) {

        if (YoriLibIsSep(DirectoryToSearch.StartOfString[PathSeperator]) ||
            DirectoryToSearch.StartOfString[PathSeperator] == ':') {

            YoriLibInitEmptyString(&FileNameToFind);
            FileNameToFind.StartOfString = &DirectoryToSearch.StartOfString[PathSeperator + 1];
            FileNameToFind.LengthInChars = DirectoryToSearch.LengthInChars - PathSeperator - 1;

            //
            //  If the seperator is a slash, and it's in the middle of a
            //  path specification, remove it.  If the seperator is a slash
            //  but it indicates the root of a drive, or if it's a colon,
            //  retain it.
            //

            if (YoriLibIsSep(DirectoryToSearch.StartOfString[PathSeperator])) {
                if (PathSeperator == 2 &&
                    YoriLibIsDriveLetterWithColonAndSlash(&DirectoryToSearch)) {

                    DirectoryToSearch.LengthInChars = PathSeperator + 1;
                } else if (PathSeperator == 6 &&
                           YoriLibIsPrefixedDriveLetterWithColonAndSlash(&DirectoryToSearch)) {
                    DirectoryToSearch.LengthInChars = PathSeperator + 1;
                } else {
                    DirectoryToSearch.LengthInChars = PathSeperator;
                }
            } else {
                DirectoryToSearch.LengthInChars = PathSeperator + 1;
            }
            break;
        } else if (PathSeperator == 0) {
            ASSERT(PathSeperator > 0);
            YoriLibFree(PathExtComponents);
            return FALSE;
        }
    }

    YoriLibInitEmptyString(&ScratchArea);

    if (!YoriLibLocateFileExtensionsInOnePath(&FileNameToFind,
                                              &DirectoryToSearch,
                                              &ScratchArea,
                                              PathExtComponents,
                                              PathExtCount,
                                              MatchAllCallback,
                                              MatchAllContext,
                                              FoundPath,
                                              FALSE)) {

        YoriLibFreeStringContents(&ScratchArea);
        YoriLibPathFreePathExtComponents(PathExtComponents, PathExtCount);
        return FALSE;
    }

    YoriLibFreeStringContents(&ScratchArea);
    YoriLibPathFreePathExtComponents(PathExtComponents, PathExtCount);
    return TRUE;
}

/**
 Perform a path search for a file with a known extension that could be in any
 location.

 @param SearchFor The file name to search for.

 @param PathVariable The contents of the environment variable to search through.

 @param MatchAllCallback Optional callback to be invoked on every single match.
        If not specified, this function returns the first match.

 @param MatchAllContext Optional context to pass to the callback, if it is
        specified.

 @param FoundPath If not specified, this string is updated to contain any
        matching file.

 @return TRUE to indicate the lookup was successful, and FALSE to indicate a
         lookup failure.  Success does not imply a match was found; if a
         lookup successfully found nothing, FoundPath will contain an empty
         string.
 */
__success(return)
BOOL
YoriLibPathLocateKnownExtensionUnknownLocation(
    __in PYORI_STRING SearchFor,
    __in PYORI_STRING PathVariable,
    __in_opt PYORI_LIB_PATH_MATCH_FN MatchAllCallback,
    __in_opt PVOID MatchAllContext,
    __inout PYORI_STRING FoundPath
    )
{

    YORI_STRING ScratchArea;

    if (!YoriLibAllocateString(&ScratchArea, SearchFor->LengthAllocated + 256)) {
        return FALSE;
    }

    //
    //  If we have a fully specified extension, just look for it directly.
    //

    YoriLibSearchEnv(SearchFor, PathVariable, &ScratchArea, MatchAllCallback, MatchAllContext, FoundPath, FALSE);
    YoriLibFreeStringContents(&ScratchArea);
    return TRUE;
}


/**
 Search for a file name within the path.  If it's found, output the string
 matching.  The caller is expected to free this string with
 @ref YoriLibDereference .

 @param SearchFor The file name to search for.

 @param MatchAllCallback An optional callback to invoke each time a
        candidate match is found.

 @param MatchAllContext Context information to supply to MatchAllCallback
        if it is specified.

 @param PathName On successful completion, updated to point to a newly
        allocated string matching the next match.

 @return TRUE to indicate a match was found, FALSE if it was not.
 */
__success(return)
BOOL
YoriLibLocateExecutableInPath(
    __in PYORI_STRING SearchFor,
    __in_opt PYORI_LIB_PATH_MATCH_FN MatchAllCallback,
    __in_opt PVOID MatchAllContext,
    __out _When_(MatchAllCallback != NULL, _Post_invalid_) PYORI_STRING PathName
    )
{
    BOOL SearchPathExt = TRUE;
    BOOL SearchPath = TRUE;
    YORI_STRING FoundPath;
    YORI_STRING PathData;
    DWORD PathLength;
    DWORD CurDirLength;

    ASSERT(YoriLibIsStringNullTerminated(SearchFor));

    //
    //  We have four cases:
    //  1. The path is fully specified, and we have nothing to do.
    //  2. There is a path component, but no extension, so we need
    //     to search PATHEXT only.
    //  3. There is an extension, but no path component, so we need
    //     to search PATH only.
    //  4. There is neither a path component nor extension, so we
    //     need to search PATH and PATHEXT.
    //
    //  To achieve this we scan backwards through the name, looking for a
    //  period or seperator.  If we see a period before any seperator, we
    //  have an extension; if we see any seperator anywhere we have a path.
    //

    PathLength = SearchFor->LengthInChars;
    while (PathLength > 0) {
        PathLength--;
        if (SearchFor->StartOfString[PathLength] == '.') {

            if (SearchPath) {
                SearchPathExt = FALSE;
            }

        } else if (YoriLibIsSep(SearchFor->StartOfString[PathLength]) ||
                   SearchFor->StartOfString[PathLength] == ':') {

            SearchPath = FALSE;
            break;
        }
    }

    YoriLibInitEmptyString(&FoundPath);

    //
    //  If we're not searching PATH or PATHEXT, then just check if the string
    //  resolves to anything.  If it does, return the string so as to indicate
    //  it can be executed.  If it doesn't, try appending the PathExt
    //  extensions to it and see if it's located.
    //

    if (!SearchPath && !SearchPathExt) {

        HANDLE hFind;
        DWORD Index;
        WIN32_FIND_DATA FindData;
        YORI_STRING SearchDirectory;
        YORI_STRING FoundFile;

        hFind = FindFirstFile(SearchFor->StartOfString, &FindData);

        if (hFind != INVALID_HANDLE_VALUE) {

            FindClose(hFind);

            //
            //  Search backwards to find if there's a seperator.  That char
            //  and all before it are the directory part.
            //

            SearchDirectory.StartOfString = SearchFor->StartOfString;
            SearchDirectory.LengthInChars = 0;
            for (Index = SearchFor->LengthInChars; Index > 0; Index--) {
                if (YoriLibIsSep(SearchFor->StartOfString[Index - 1])) {
                    SearchDirectory.LengthInChars = Index;
                    break;
                }
            }

            //
            //  Take the file part from the enumerate.
            //

            YoriLibConstantString(&FoundFile, FindData.cFileName);

            if (!YoriLibAllocateString(PathName, SearchDirectory.LengthInChars + FoundFile.LengthInChars + 1)) {
                return FALSE;
            }
            PathName->LengthInChars = YoriLibSPrintf(PathName->StartOfString, _T("%y%y"), &SearchDirectory, &FoundFile);
            if (MatchAllCallback) {
                if (!MatchAllCallback(PathName, MatchAllContext)) {
                    YoriLibFreeStringContents(PathName);
                    return FALSE;
                } else {
                    YoriLibFreeStringContents(PathName);
                    return TRUE;
                }
            }
            return TRUE;
        }
        SearchPathExt = TRUE;
    }

    YoriLibInitEmptyString(PathName);

    //
    //  The worst case file name length is the size of the prefix, plus the
    //  PATH variable or current directory, a seperator, and the longest
    //  possible file name component in Windows, which is 256.
    //

    CurDirLength = GetCurrentDirectory(0, NULL);
    PathLength = GetEnvironmentVariable(_T("PATH"), NULL, 0);
    if (PathLength < CurDirLength + 1) {
        PathLength = CurDirLength + 1;
    }
    if (PathLength < MAX_PATH) {
        PathLength = MAX_PATH;
    }

    PathLength += sizeof("\\\\?\\");

    if (!YoriLibAllocateString(&FoundPath, PathLength + 256)) {
        return FALSE;
    }

    //
    //  The contents of the PATH environment variable.
    //

    if (!YoriLibAllocateString(&PathData, PathLength)) {
        YoriLibFreeStringContents(&FoundPath);
        return FALSE;
    }

    PathData.StartOfString[0] = '\0';
    PathData.LengthInChars = GetEnvironmentVariable(_T("PATH"), PathData.StartOfString, PathData.LengthAllocated);

    if (SearchPath && SearchPathExt) {
        if (!YoriLibPathLocateUnknownExtensionUnknownLocation(SearchFor, &PathData, MatchAllCallback, MatchAllContext, &FoundPath)) {
            YoriLibFreeStringContents(&FoundPath);
            YoriLibFreeStringContents(&PathData);
            return FALSE;
        }
    } else if (SearchPath) {

        if (!YoriLibPathLocateKnownExtensionUnknownLocation(SearchFor, &PathData, MatchAllCallback, MatchAllContext, &FoundPath)) {
            YoriLibFreeStringContents(&FoundPath);
            YoriLibFreeStringContents(&PathData);
            return FALSE;
        }

        if (MatchAllCallback != NULL || FoundPath.StartOfString[0] == '\0') {
            if (!YoriLibPathLocateUnknownExtensionUnknownLocation(SearchFor, &PathData, MatchAllCallback, MatchAllContext, &FoundPath)) {
                YoriLibFreeStringContents(&FoundPath);
                YoriLibFreeStringContents(&PathData);
                return FALSE;
            }
        }

    } else if (SearchPathExt) {

        if (!YoriLibPathLocateUnknownExtensionKnownLocation(SearchFor, MatchAllCallback, MatchAllContext, &FoundPath)) {
            YoriLibFreeStringContents(&FoundPath);
            YoriLibFreeStringContents(&PathData);
            return FALSE;
        }
    }

    YoriLibFreeStringContents(&PathData);

    if (MatchAllCallback != NULL) {
        YoriLibFreeStringContents(&FoundPath);
    } else {
        memcpy(PathName, &FoundPath, sizeof(YORI_STRING));
        ASSERT(YoriLibIsStringNullTerminated(PathName));
    }
    return TRUE;
}

// vim:sw=4:ts=4:et:


<|start_filename|>sh/builtin.c<|end_filename|>
/**
 * @file /sh/builtin.c
 *
 * Yori shell built in function handler
 *
 * Copyright (c) 2017 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "yori.h"

/**
 Invoke a different program to complete executing the command.  This may be
 Powershell for its scripts, YS for its scripts, etc.

 @param ExecContext Pointer to the exec context for a command that is known
        to be implemented by an external program.

 @param ExtraArgCount The number of extra arguments that should be inserted
        into the beginning of the ExecContext, followed by a set of NULL
        terminated strings to insert into the ExecContext.

 @return ExitCode from process, or nonzero on failure.
 */
DWORD
YoriShBuckPass (
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext,
    __in DWORD ExtraArgCount,
    ...
    )
{
    YORI_LIBSH_CMD_CONTEXT OldCmdContext;
    DWORD ExitCode = 1;
    DWORD count;
    BOOL ExecAsBuiltin = FALSE;
    va_list marker;

    memcpy(&OldCmdContext, &ExecContext->CmdToExec, sizeof(YORI_LIBSH_CMD_CONTEXT));

    ExecContext->CmdToExec.ArgC += ExtraArgCount;
    ExecContext->CmdToExec.MemoryToFree = YoriLibReferencedMalloc(ExecContext->CmdToExec.ArgC * (sizeof(YORI_STRING) + sizeof(YORI_LIBSH_ARG_CONTEXT)));
    if (ExecContext->CmdToExec.MemoryToFree == NULL) {
        memcpy(&ExecContext->CmdToExec, &OldCmdContext, sizeof(YORI_LIBSH_CMD_CONTEXT));
        return ExitCode;
    }

    ExecContext->CmdToExec.ArgV = ExecContext->CmdToExec.MemoryToFree;

    ExecContext->CmdToExec.ArgContexts = (PYORI_LIBSH_ARG_CONTEXT)YoriLibAddToPointer(ExecContext->CmdToExec.ArgV, sizeof(YORI_STRING) * ExecContext->CmdToExec.ArgC);
    ZeroMemory(ExecContext->CmdToExec.ArgContexts, sizeof(YORI_LIBSH_ARG_CONTEXT) * ExecContext->CmdToExec.ArgC);

    va_start(marker, ExtraArgCount);
    for (count = 0; count < ExtraArgCount; count++) {
        LPTSTR NewArg = (LPTSTR)va_arg(marker, LPTSTR);
        YORI_STRING YsNewArg;
        YORI_STRING FoundInPath;

        YoriLibInitEmptyString(&ExecContext->CmdToExec.ArgV[count]);
        YoriLibConstantString(&YsNewArg, NewArg);

        //
        //  Search for the first extra argument in the path.  If we find it,
        //  execute as a program; if not, execute as a builtin.
        //

        if (count == 0) {
            YoriLibInitEmptyString(&FoundInPath);
            if (YoriLibLocateExecutableInPath(&YsNewArg, NULL, NULL, &FoundInPath) && FoundInPath.LengthInChars > 0) {
                memcpy(&ExecContext->CmdToExec.ArgV[0], &FoundInPath, sizeof(YORI_STRING));
                ASSERT(YoriLibIsStringNullTerminated(&ExecContext->CmdToExec.ArgV[0]));
                YoriLibInitEmptyString(&FoundInPath);
            } else {
                ExecAsBuiltin = TRUE;
                YoriLibConstantString(&ExecContext->CmdToExec.ArgV[count], NewArg);
            }

            YoriLibFreeStringContents(&FoundInPath);
        } else {
            YoriLibConstantString(&ExecContext->CmdToExec.ArgV[count], NewArg);
        }
    }
    va_end(marker);

    for (count = 0; count < OldCmdContext.ArgC; count++) {
        YoriLibShCopyArg(&OldCmdContext, count, &ExecContext->CmdToExec, count + ExtraArgCount);
    }

    YoriLibShCheckIfArgNeedsQuotes(&ExecContext->CmdToExec, 0);

    if (ExecAsBuiltin) {
        ExitCode = YoriShBuiltIn(ExecContext);
    } else {
        ExecContext->IncludeEscapesAsLiteral = TRUE;
        ExitCode = YoriShExecuteSingleProgram(ExecContext);
    }

    YoriLibShFreeCmdContext(&ExecContext->CmdToExec);
    memcpy(&ExecContext->CmdToExec, &OldCmdContext, sizeof(YORI_LIBSH_CMD_CONTEXT));

    return ExitCode;
}

/**
 Invoke CMD to execute a script.  This is different to regular child
 processes because CMD wants all arguments to be enclosed in quotes, in
 addition to quotes around individual arguments, as in:
 C:\\Windows\\System32\\Cmd.exe /c ""C:\\Path To\\Foo.cmd" "Arg To Foo""

 @param ExecContext Pointer to the exec context for a command that is known
        to be implemented by CMD.

 @return ExitCode from CMD, or nonzero on failure.
 */
DWORD
YoriShBuckPassToCmd (
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext
    )
{
    YORI_LIBSH_CMD_CONTEXT OldCmdContext;
    YORI_STRING CmdLine;
    DWORD ExitCode = EXIT_SUCCESS;

    memcpy(&OldCmdContext, &ExecContext->CmdToExec, sizeof(YORI_LIBSH_CMD_CONTEXT));
    YoriLibInitEmptyString(&CmdLine);
    if (!YoriLibShBuildCmdlineFromCmdContext(&OldCmdContext, &CmdLine, FALSE, NULL, NULL)) {
        return EXIT_FAILURE;
    }

    if (!YoriLibShBuildCmdContextForCmdBuckPass(&ExecContext->CmdToExec, &CmdLine)) {
        YoriLibFreeStringContents(&CmdLine);
        return EXIT_FAILURE;
    }

    YoriLibFreeStringContents(&CmdLine);
    ExecContext->IncludeEscapesAsLiteral = TRUE;
    ExitCode = YoriShExecuteSingleProgram(ExecContext);

    YoriLibShFreeCmdContext(&ExecContext->CmdToExec);
    memcpy(&ExecContext->CmdToExec, &OldCmdContext, sizeof(YORI_LIBSH_CMD_CONTEXT));

    return ExitCode;
}

/**
 Call a builtin function.  This may be in a DLL or part of the main executable,
 but it is executed synchronously via a call rather than a CreateProcess.  The
 caller is required to reparse the ExecContext to expand environment variables,
 ensuring arguments are in their correct position after expansion.

 @param Fn The function associated with the builtin operation to call.

 @param ExecContext The context surrounding this particular function.  This
        is used to establish redirection context, but the command is expected
        to have already been reparsed on function entry to perform environment
        variable expansion.

 @param ArgC The number of arguments to execute.

 @param EscapedArgV An array of arguments to pass to the builtin.

 @return ExitCode, typically zero for success, nonzero for failure.
 */
int
YoriShExecuteInProc(
    __in PYORI_CMD_BUILTIN Fn,
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext,
    __in DWORD ArgC,
    __in PYORI_STRING EscapedArgV
    )
{
    YORI_LIBSH_PREVIOUS_REDIRECT_CONTEXT PreviousRedirectContext;
    BOOLEAN WasPipe = FALSE;
    PYORI_STRING NoEscapedArgV;
    PYORI_STRING SavedEscapedArgV;
    DWORD SavedEscapedArgC;
    DWORD Count;
    DWORD ExitCode = 0;

    NoEscapedArgV = NULL;

    //
    //  Remove the escapes from the command line.  This allows the builtin to
    //  have access to the escaped form if required.
    //

    NoEscapedArgV = YoriLibReferencedMalloc(ArgC * sizeof(YORI_STRING));
    if (NoEscapedArgV == NULL) {
        ExitCode = ERROR_OUTOFMEMORY;
        goto Cleanup;
    }

    for (Count = 0; Count < ArgC; Count++) {
        YoriLibCloneString(&NoEscapedArgV[Count], &EscapedArgV[Count]);
    }

    if (!YoriLibShRemoveEscapesFromArgCArgV(ArgC, NoEscapedArgV)) {
        ExitCode = ERROR_OUTOFMEMORY;
        goto Cleanup;
    }

    //
    //  We execute builtins on a single thread due to the amount of
    //  process wide state that could get messed up if we don't (eg.
    //  stdout.)  Unfortunately this means we can't natively implement
    //  things like pipe from builtins, because the builtin has to
    //  finish before the next process can start.  So if a pipe is
    //  requested, convert it into a buffer, and let the process
    //  finish.
    //

    if (ExecContext->StdOutType == StdOutTypePipe) {
        WasPipe = TRUE;
        ExecContext->StdOutType = StdOutTypeBuffer;
    }

    ExitCode = YoriLibShInitializeRedirection(ExecContext, TRUE, &PreviousRedirectContext);
    if (ExitCode != ERROR_SUCCESS) {
        goto Cleanup;
    }

    //
    //  Unlike external processes, builtins need to start buffering
    //  before they start to ensure that output during execution has
    //  somewhere to go.
    //

    if (ExecContext->StdOutType == StdOutTypeBuffer) {
        if (ExecContext->StdOut.Buffer.ProcessBuffers != NULL) {
            if (YoriLibShAppendToExistingProcessBuffer(ExecContext)) {
                ExecContext->StdOut.Buffer.PipeFromProcess = NULL;
            } else {
                ExecContext->StdOut.Buffer.ProcessBuffers = NULL;
            }
        } else {
            if (YoriLibShCreateNewProcessBuffer(ExecContext)) {
                ExecContext->StdOut.Buffer.PipeFromProcess = NULL;
            }
        }
    }

    SavedEscapedArgC = YoriShGlobal.EscapedArgC;
    SavedEscapedArgV = YoriShGlobal.EscapedArgV;
    YoriShGlobal.EscapedArgC = ArgC;
    YoriShGlobal.EscapedArgV = EscapedArgV;
    YoriShGlobal.RecursionDepth++;
    ExitCode = Fn(ArgC, NoEscapedArgV);
    YoriShGlobal.RecursionDepth--;
    YoriShGlobal.EscapedArgC = SavedEscapedArgC;
    YoriShGlobal.EscapedArgV = SavedEscapedArgV;
    YoriLibShRevertRedirection(&PreviousRedirectContext);

    if (WasPipe) {
        YoriLibShForwardProcessBufferToNextProcess(ExecContext);
    } else {

        //
        //  Once the process has completed, if it's outputting to
        //  buffers, wait for the buffers to contain final data.
        //

        if (ExecContext->StdOutType == StdOutTypeBuffer &&
            ExecContext->StdOut.Buffer.ProcessBuffers != NULL)  {

            YoriLibShWaitForProcessBufferToFinalize(ExecContext->StdOut.Buffer.ProcessBuffers);
        }

        if (ExecContext->StdErrType == StdErrTypeBuffer &&
            ExecContext->StdErr.Buffer.ProcessBuffers != NULL) {

            YoriLibShWaitForProcessBufferToFinalize(ExecContext->StdErr.Buffer.ProcessBuffers);
        }
    }

Cleanup:

    if (NoEscapedArgV != NULL) {
        for (Count = 0; Count < ArgC; Count++) {
            YoriLibFreeStringContents(&NoEscapedArgV[Count]);
        }
        YoriLibDereference(NoEscapedArgV);
    }

    return ExitCode;
}

/**
 Execute a command contained in a DLL file.

 @param ModuleFileName Pointer to the full path to the DLL file.

 @param ExecContext Specifies all of the arguments that should be passed to
        the module.

 @param ExitCode On successful completion, updated to point to the exit code
        from the module.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOL
YoriShExecuteNamedModuleInProc(
    __in LPTSTR ModuleFileName,
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext,
    __out PDWORD ExitCode
    )
{
    PYORI_CMD_BUILTIN Main;
    PYORI_LIBSH_LOADED_MODULE Module;

    Module = YoriLibShLoadDll(ModuleFileName);
    if (Module == NULL) {
        return FALSE;
    }

    Main = (PYORI_CMD_BUILTIN)GetProcAddress(Module->ModuleHandle, "YoriMain");
    if (Main == NULL) {
        YoriLibShReleaseDll(Module);
        return FALSE;
    }

    if (Main) {
        PYORI_LIBSH_LOADED_MODULE PreviousModule;
        YORI_STRING CmdLine;
        DWORD ArgC;
        DWORD Count;
        PYORI_STRING EscapedArgV;

        //
        //  Build a command line, leaving all escapes in the command line.
        //

        YoriLibInitEmptyString(&CmdLine);
        if (!YoriLibShBuildCmdlineFromCmdContext(&ExecContext->CmdToExec, &CmdLine, FALSE, NULL, NULL)) {
            YoriLibShReleaseDll(Module);
            return FALSE;
        }

        //
        //  Parse the command line in the same way that a child process would.
        //

        ASSERT(YoriLibIsStringNullTerminated(&CmdLine));
        EscapedArgV = YoriLibCmdlineToArgcArgv(CmdLine.StartOfString, (DWORD)-1, TRUE, &ArgC);
        YoriLibFreeStringContents(&CmdLine);

        if (EscapedArgV == NULL) {
            YoriLibShReleaseDll(Module);
            return FALSE;
        }

        PreviousModule = YoriLibShGetActiveModule();
        YoriLibShSetActiveModule(Module);
        *ExitCode = YoriShExecuteInProc(Main, ExecContext, ArgC, EscapedArgV);
        YoriLibShSetActiveModule(PreviousModule);

        for (Count = 0; Count < ArgC; Count++) {
            YoriLibFreeStringContents(&EscapedArgV[Count]);
        }
        YoriLibDereference(EscapedArgV);
    }

    YoriLibShReleaseDll(Module);
    return TRUE;
}


/**
 Execute a function if we can't find it in the PATH.  Because Yori looks
 for programs in the path first, this function acts as a "last chance" to
 see if there's an internal implementation before we give up and fail.

 @param ExecContext Pointer to the exec context for this program.

 @return ExitCode, typically zero for success or nonzero on failure.
 */
DWORD
YoriShBuiltIn (
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext
    )
{
    DWORD ExitCode = 1;
    PYORI_CMD_BUILTIN BuiltInCmd = NULL;
    PYORI_LIBSH_BUILTIN_CALLBACK CallbackEntry = NULL;
    YORI_STRING CmdLine;
    DWORD ArgC;
    DWORD Count;
    PYORI_STRING EscapedArgV;

    //
    //  Build a command line, leaving all escapes in the command line.
    //

    YoriLibInitEmptyString(&CmdLine);
    if (!YoriLibShBuildCmdlineFromCmdContext(&ExecContext->CmdToExec, &CmdLine, FALSE, NULL, NULL)) {
        ExitCode = ERROR_OUTOFMEMORY;
        return ExitCode;
    }

    //
    //  Parse the command line in the same way that a child process would.
    //

    ASSERT(YoriLibIsStringNullTerminated(&CmdLine));
    EscapedArgV = YoriLibCmdlineToArgcArgv(CmdLine.StartOfString, (DWORD)-1, TRUE, &ArgC);
    YoriLibFreeStringContents(&CmdLine);

    if (EscapedArgV == NULL) {
        ExitCode = ERROR_OUTOFMEMORY;
        return ExitCode;
    }

    CallbackEntry = YoriLibShLookupBuiltinByName(&EscapedArgV[0]);
    if (CallbackEntry != NULL) {
        BuiltInCmd = CallbackEntry->BuiltInFn;
    }

    if (BuiltInCmd) {
        PYORI_LIBSH_LOADED_MODULE PreviousModule;
        PYORI_LIBSH_LOADED_MODULE HostingModule = NULL;

        ASSERT(CallbackEntry != NULL);

        //
        //  If the function is in a module, reference the DLL to keep it alive
        //  until it returns.
        //

        if (CallbackEntry != NULL && CallbackEntry->ReferencedModule != NULL) {
            HostingModule = CallbackEntry->ReferencedModule;
            YoriLibShReferenceDll(HostingModule);
        }

        //
        //  Indicate which module is currently executing, and execute from it.
        //

        PreviousModule = YoriLibShGetActiveModule();
        YoriLibShSetActiveModule(HostingModule);
        ExitCode = YoriShExecuteInProc(BuiltInCmd, ExecContext, ArgC, EscapedArgV);
        ASSERT(YoriLibShGetActiveModule() == HostingModule);
        YoriLibShSetActiveModule(PreviousModule);

        if (HostingModule != NULL) {
            YoriLibShReleaseDll(HostingModule);
        }
    } else {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Unrecognized command: %y\n"), &EscapedArgV[0]);
        if (ExecContext->StdOutType == StdOutTypePipe &&
            ExecContext->NextProgram != NULL &&
            ExecContext->NextProgram->StdInType == StdInTypePipe) {

            ExecContext->NextProgramType = NextProgramExecNever;
        }
    }

    for (Count = 0; Count < ArgC; Count++) {
        YoriLibFreeStringContents(&EscapedArgV[Count]);
    }
    YoriLibDereference(EscapedArgV);

    return ExitCode;
}

/**
 Execute a command that is built in to the shell.  This can be used by in
 process extension modules.

 @param Expression The expression to execute.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOL
YoriShExecuteBuiltinString(
    __in PYORI_STRING Expression
    )
{
    YORI_LIBSH_EXEC_PLAN ExecPlan;
    YORI_LIBSH_CMD_CONTEXT CmdContext;
    PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext;

    //
    //  Parse the expression we're trying to execute.
    //

    if (!YoriLibShParseCmdlineToCmdContext(Expression, 0, &CmdContext)) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Parse error\n"));
        return FALSE;
    }

    if (CmdContext.ArgC == 0) {
        YoriLibShFreeCmdContext(&CmdContext);
        return FALSE;
    }

    if (!YoriShExpandEnvironmentInCmdContext(&CmdContext)) {
        YoriLibShFreeCmdContext(&CmdContext);
        return FALSE;
    }

    if (!YoriLibShParseCmdContextToExecPlan(&CmdContext, &ExecPlan, NULL, NULL, NULL, NULL)) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Parse error\n"));
        YoriLibShFreeCmdContext(&CmdContext);
        return FALSE;
    }

    ExecContext = ExecPlan.FirstCmd;

    if (ExecContext->NextProgram != NULL) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Attempt to invoke multi component expression as a builtin\n"));
        YoriLibShFreeExecPlan(&ExecPlan);
        YoriLibShFreeCmdContext(&CmdContext);
        return FALSE;
    }

    YoriShGlobal.ErrorLevel = YoriShBuiltIn(ExecContext);

    YoriLibShFreeExecPlan(&ExecPlan);
    YoriLibShFreeCmdContext(&CmdContext);

    return TRUE;
}

// vim:sw=4:ts=4:et:


<|start_filename|>lib/iconv.c<|end_filename|>
/**
 * @file lib/iconv.c
 *
 * Yori text encoding routines
 *
 * Copyright (c) 2017 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "yoripch.h"
#include "yorilib.h"

/**
 The current output encoding.  Only meaningful if
 YoriLibActiveOutputEncodingInitialized is TRUE.
 */
DWORD YoriLibActiveOutputEncoding;

/**
 The current input encoding.  Only meaningful if
 YoriLibActiveInputEncodingInitialized is TRUE.
 */
DWORD YoriLibActiveInputEncoding;

/**
 Set to TRUE once the default output encoding has been established.
 */
BOOLEAN YoriLibActiveOutputEncodingInitialized;

/**
 Set to TRUE once the default input encoding has been established.
 */
BOOLEAN YoriLibActiveInputEncodingInitialized;

/**
 Return TRUE if the system supports UTF-8.  This is supported on NT 4 and
 newer.
 */
BOOLEAN
YoriLibIsUtf8Supported(VOID)
{
    DWORD WinMajorVer;
    DWORD WinMinorVer;
    DWORD BuildNumber;

    YoriLibGetOsVersion(&WinMajorVer, &WinMinorVer, &BuildNumber);

    if (WinMajorVer < 4) {
        return FALSE;
    }

    return TRUE;
}

/**
 Returns the active output encoding, establishing the default encoding if it
 has not yet been determined.  Currently the default is UTF8 except on old
 releases where this support is not available.
 */
DWORD
YoriLibGetMultibyteOutputEncoding(VOID)
{
    if (!YoriLibActiveOutputEncodingInitialized) {
        if (YoriLibIsUtf8Supported()) {
            YoriLibActiveOutputEncoding = CP_UTF8;
        } else {
            YoriLibActiveOutputEncoding = CP_OEMCP;
        }
        YoriLibActiveOutputEncodingInitialized = TRUE;
    }
    return YoriLibActiveOutputEncoding;
}

/**
 Returns the active input encoding, establishing the default encoding if it
 has not yet been determined.  Currently the default is UTF8 except on old
 releases where this support is not available.
 */
DWORD
YoriLibGetMultibyteInputEncoding(VOID)
{
    if (!YoriLibActiveInputEncodingInitialized) {
        if (YoriLibIsUtf8Supported()) {
            YoriLibActiveInputEncoding = CP_UTF8;
        } else {
            YoriLibActiveInputEncoding = CP_OEMCP;
        }
        YoriLibActiveInputEncodingInitialized = TRUE;
    }
    return YoriLibActiveInputEncoding;
}

/**
 Set the output encoding to a specific value.

 @param Encoding The new encoding to use.
 */
VOID
YoriLibSetMultibyteOutputEncoding(
    __in DWORD Encoding
    )
{
    YoriLibActiveOutputEncoding = Encoding;
    YoriLibActiveOutputEncodingInitialized = TRUE;
}

/**
 Set the input encoding to a specific value.

 @param Encoding The new encoding to use.
 */
VOID
YoriLibSetMultibyteInputEncoding(
    __in DWORD Encoding
    )
{
    YoriLibActiveInputEncoding = Encoding;
    YoriLibActiveInputEncodingInitialized = TRUE;
}

/**
 Returns the number of bytes needed to store a specified UTF16 string in
 the current output encoding.

 @param StringBuffer The UTF16 string.

 @param BufferLength The length of the string, in characters.

 @return The number of bytes needed to store the output form.
 */
DWORD
YoriLibGetMultibyteOutputSizeNeeded(
    __in LPCTSTR StringBuffer,
    __in DWORD BufferLength
    )
{
    DWORD Return;
    DWORD Encoding = YoriLibGetMultibyteOutputEncoding();
    if (Encoding == CP_UTF16) {
        return BufferLength * sizeof(WCHAR);
    }
    Return = WideCharToMultiByte(Encoding, 0, StringBuffer, BufferLength, NULL, 0, NULL, NULL);
    ASSERT(Return > 0 || BufferLength == 0);
    return Return;
}

#if defined(_MSC_VER) && (_MSC_VER >= 1700)
#pragma warning(disable: 6054) // Buffer might not be NULL terminated.
                               // This occurs when UTF16 invokes memcpy
                               // and trusts the caller to provide sane
                               // input
#endif

/**
 Convert a UTF16 string into the output encoding.

 @param InputStringBuffer Pointer to a UTF16 string.

 @param InputBufferLength The size of InputStringBuffer, in characters.

 @param OutputStringBuffer Pointer to a buffer to be populated with the string
        in the current output encoding.

 @param OutputBufferLength The length of the output buffer, in bytes.
 */
VOID
YoriLibMultibyteOutput(
    __in_ecount(InputBufferLength) LPCTSTR InputStringBuffer,
    __in DWORD InputBufferLength,
    __out_ecount(OutputBufferLength) LPSTR OutputStringBuffer,
    __in DWORD OutputBufferLength
    )
{
    DWORD Return;
    DWORD Encoding = YoriLibGetMultibyteOutputEncoding();
    if (Encoding == CP_UTF16) {
        ASSERT(OutputBufferLength >= InputBufferLength * sizeof(WCHAR));
        if (OutputBufferLength >= InputBufferLength * sizeof(WCHAR)) {
            memcpy(OutputStringBuffer, InputStringBuffer, InputBufferLength * sizeof(WCHAR));
        }
        return;
    }
    Return = WideCharToMultiByte(Encoding,
                                 0,
                                 InputStringBuffer,
                                 InputBufferLength,
                                 OutputStringBuffer,
                                 OutputBufferLength,
                                 NULL,
                                 NULL);

    if (Return == 0) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("InputBufferLength %i OutputBufferLength %i\n"), InputBufferLength, OutputBufferLength);
        ASSERT(Return != 0);
    }
}

/**
 Returns the number of bytes needed to store a string in the current input
 encoding into UTF16.

 @param StringBuffer The string in the input encoding.

 @param BufferLength The length of the string, in bytes.

 @return The number of bytes needed to store the UTF16 form.
 */
DWORD
YoriLibGetMultibyteInputSizeNeeded(
    __in LPCSTR StringBuffer,
    __in DWORD BufferLength
    )
{
    DWORD Encoding = YoriLibGetMultibyteInputEncoding();
    if (Encoding == CP_UTF16) {
        return BufferLength;
    }
    return MultiByteToWideChar(Encoding, 0, StringBuffer, BufferLength, NULL, 0);
}

/**
 Convert a string from the input encoding into UTF16.

 @param InputStringBuffer Pointer to a string in input encoding form.

 @param InputBufferLength The size of InputStringBuffer, in bytes.

 @param OutputStringBuffer Pointer to a buffer to be populated with the string
        in UTF16 format.

 @param OutputBufferLength The length of the output buffer, in characters.
 */
VOID
YoriLibMultibyteInput(
    __in_ecount(InputBufferLength) LPCSTR InputStringBuffer,
    __in DWORD InputBufferLength,
    __out_ecount(OutputBufferLength) LPTSTR OutputStringBuffer,
    __in DWORD OutputBufferLength
    )
{
    DWORD Return;
    DWORD Encoding = YoriLibGetMultibyteInputEncoding();
    if (Encoding == CP_UTF16) {
        ASSERT(OutputBufferLength >= InputBufferLength);
        if (OutputBufferLength >= InputBufferLength) {
            memcpy(OutputStringBuffer, InputStringBuffer, InputBufferLength * sizeof(WCHAR));
        }
        return;
    }
    Return = MultiByteToWideChar(Encoding,
                                 0,
                                 InputStringBuffer,
                                 InputBufferLength,
                                 OutputStringBuffer,
                                 OutputBufferLength);

    ASSERT(Return != 0);
}

// vim:sw=4:ts=4:et:


<|start_filename|>more/more.h<|end_filename|>
/**
 * @file more/more.h
 *
 * Yori shell display master header
 *
 * Copyright (c) 2017-2018 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 Data describing a physical line.  A physical line is a line of text from the
 data source, which may take more characters than fit on a viewport line.
 */
typedef struct _MORE_PHYSICAL_LINE {

    /**
     A list of physical lines.  Paired with MORE_CONTEXT::PhysicalLineList
     and synchronized with MORE_CONTEXT::PhysicalLineMutex .
     */
    YORI_LIST_ENTRY LineList;

    /**
     Pointer to the referenced allocation that contains this physical line.
     */
    PVOID MemoryToFree;

    /**
     The color attribute to display at the beginning of the line.
     */
    WORD InitialColor;

    /**
     The number of this physical line within the input stream.  The first
     line is zero.
     */
    DWORDLONG LineNumber;

    /**
     The contents of the physical line.
     */
    YORI_STRING LineContents;
} MORE_PHYSICAL_LINE, *PMORE_PHYSICAL_LINE;

/**
 A logical line, meaning a line rendered for display on the console.
 */
typedef struct _MORE_LOGICAL_LINE {

    /**
     Pointer to the physical line whose data is being decomposed into this
     logical line.
     */
    PMORE_PHYSICAL_LINE PhysicalLine;

    /**
     The zero based index of this logical line from within the corresponding
     physical line.
     */
    DWORD LogicalLineIndex;

    /**
     The offset in characters from the PhysicalLine to the beginning of the
     string represented by this logical line.
     */
    DWORD PhysicalLineCharacterOffset;

    /**
     Characters remaining in any search match, if a search match commenced
     on a previous logical line from the same physical line.
     */
    DWORD CharactersRemainingInMatch;

    /**
     The color attribute to display at the beginning of the line.
     */
    WORD InitialDisplayColor;

    /**
     The color attribute at the beginning of the line as indicated by the
     input stream.  This can be different to display color where things like
     search are used which changes the color of the input stream.
     */
    WORD InitialUserColor;

    /**
     If TRUE, an explicit newline should be added after this line.  If FALSE,
     the console auto wraps and no newline should be issued.
     */
    BOOLEAN ExplicitNewlineRequired;

    /**
     If TRUE, there are more logical lines to follow this one that are derived
     from the same physical line.  If FALSE, this logical line is the end of
     the physical line.
     */
    BOOLEAN MoreLogicalLines;

    /**
     The string representation of the logical line.
     */
    YORI_STRING Line;
} MORE_LOGICAL_LINE, *PMORE_LOGICAL_LINE;

/**
 Context passed to the callback which is invoked for each file found.
 */
typedef struct _MORE_CONTEXT {

    /**
     A linked list of physical lines.
     */
    YORI_LIST_ENTRY PhysicalLineList;

    /**
     Synchronization around PhysicalLineList.
     */
    HANDLE PhysicalLineMutex;

    /**
     An event that is signalled when new lines are added to the
     PhysicalLineList in case the viewport thread wants to update display
     when lines are added.
     */
    HANDLE PhysicalLineAvailableEvent;

    /**
     An event that is signalled when the ingest process should be terminated
     quickly and the application should exit.
     */
    HANDLE ShutdownEvent;


    /**
     The current width of the window, in characters.
     */
    DWORD ViewportWidth;

    /**
     The current height of the window, in lines.  Note this corresponds to the
     number of elements in ViewportLines so updating it implies a
     reallocation.
     */
    DWORD ViewportHeight;

    /**
     Description of the current selected region.
     */
    YORILIB_SELECTION Selection;

    /**
     An array of size ViewportHeight of lines currently displayed.  Note these
     refer to the strings in the PhysicalLineList.
     */
    PMORE_LOGICAL_LINE DisplayViewportLines;

    /**
     An array of size ViewportHeight of lines that are being constructed to
     display in future.  Note these refer to the strings in the
     PhysicalLineList.
     */
    PMORE_LOGICAL_LINE StagingViewportLines;

    /**
     Specifies the total number of ingested lines when the status line was
     last calculated.
     */
    DWORDLONG TotalLinesInViewportStatus;

    /**
     Specifies the number of lines within DisplayViewportLines that are
     currently populated with data.  Since population is a process, this
     starts at zero and counts up to ViewportHeight.
     */
    DWORD LinesInViewport;

    /**
     The number of lines that have been displayed as part of a single page.
     If the user hits space or similar, this value is reset such that
     another ViewportHeight number of lines is processed.
     */
    DWORD LinesInPage;

    /**
     The number of command line arguments to use as input.  This can be zero
     if input is coming from a pipe.
     */
    DWORD InputSourceCount;

    /**
     The number of spaces to display for every tab.
     */
    DWORD TabWidth;

    /**
     The color attribute to display at the beginning of the application.
     */
    WORD InitialColor;

    /**
     The color to use to highlight search terms.
     */
    WORD SearchColor;

    /**
     Pointer to an array of length InputSourceCount for file specifications
     to process.
     */
    PYORI_STRING InputSources;

    /**
     TRUE if we are in search mode, meaning that keystrokes will be applied
     to the SearchString below.  FALSE if keystrokes imply navigation.
     */
    BOOL SearchMode;

    /**
     TRUE if the status line needs to be redrawn as a result of a search
     string change.
     */
    BOOL SearchDirty;

    /**
     A string describing any text to search for.
     */
    YORI_STRING SearchString;

    /**
     Handle to the thread that is adding to the physical line array.
     */
    HANDLE IngestThread;

    /**
     TRUE if the display implies that text at the last cell in a line auto
     wraps to the next line.  This behavior is generally undesirable on NT,
     because it updates the attributes of the new line to match the final
     cell on the previous line, and resizing the window will display the
     text in a different location.  On Nano, we have no choice and need to
     operate in this degraded way.
     */
    BOOLEAN AutoWrapAtLineEnd;

    /**
     TRUE if the set of files should be enumerated recursively.  FALSE if they
     should be interpreted as files and not recursed.
     */
    BOOLEAN Recursive;

    /**
     TRUE if enumeration should not expand {}, [], or similar operators.
     FALSE if these should be expanded.
     */
    BOOLEAN BasicEnumeration;

    /**
     TRUE if the display should be the debug version which clears the screen
     and dumps the internal buffer on any display change.  This helps to
     clarify the state of the system, but is much slower than just telling
     the console about changes and moving things in the console buffer.
     */
    BOOLEAN DebugDisplay;

    /**
     TRUE if out of memory occurred and viewport can't intelligently keep
     displaying results.  This can happen because there's no memory to
     ingest more data, or because the data we have cannot be rendered.
     */
    BOOLEAN OutOfMemory;

    /**
     TRUE if the user has pressed Ctrl+Q in order to suspend pagination.
     */
    BOOLEAN SuspendPagination;

    /**
     TRUE if when reading files, this program should continually wait for
     more data to be added.  This is useful where a file is being extended
     continually by another program, but it implies that this program cannot
     move to the next file.  FALSE if this program should read until the end
     of each file and move to the next.
     */
    BOOLEAN WaitForMore;

    /**
     Records the total number of files processed.
     */
    DWORDLONG FilesFound;

    /**
     Records the total number of lines processed.
     */
    DWORDLONG LineCount;

} MORE_CONTEXT, *PMORE_CONTEXT;

VOID
MoreGetViewportDimensions(
    __in PCONSOLE_SCREEN_BUFFER_INFO ScreenInfo,
    __out PDWORD ViewportWidth,
    __out PDWORD ViewportHeight
    );

__success(return)
BOOL
MoreAllocateViewportStructures(
    __in DWORD ViewportWidth,
    __in DWORD ViewportHeight,
    __out PMORE_LOGICAL_LINE * DisplayViewportLines,
    __out PMORE_LOGICAL_LINE * StagingViewportLines
    );

BOOL
MoreInitContext(
    __inout PMORE_CONTEXT MoreContext,
    __in DWORD ArgCount,
    __in_opt PYORI_STRING ArgStrings,
    __in BOOLEAN Recursive,
    __in BOOLEAN BasicEnumeration,
    __in BOOLEAN DebugDisplay,
    __in BOOLEAN SuspendPagination,
    __in BOOLEAN WaitForMore
    );

VOID
MoreCleanupContext(
    __inout PMORE_CONTEXT MoreContext
    );

VOID
MoreGracefulExit(
    __inout PMORE_CONTEXT MoreContext
    );

DWORD WINAPI
MoreIngestThread(
    __in LPVOID Context
    );

BOOL
MoreViewportDisplay(
    __inout PMORE_CONTEXT MoreContext
    );

// vim:sw=4:ts=4:et:


<|start_filename|>shutdn/shutdn.c<|end_filename|>
/**
 * @file shutdn/shutdn.c
 *
 * Yori shell shutdown the system.
 *
 * Copyright (c) 2019 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 Help text to display to the user.
 */
const
CHAR strShutdownHelpText[] =
        "\n"
        "Shutdown the system.\n"
        "\n"
        "SHUTDN [-license] [-f] [-k|-l|-r|-s [-p]]\n"
        "\n"
        "   -f             Force the action without waiting for programs to close cleanly\n"
        "   -k             Lock the current session\n"
        "   -l             Log off the current user\n"
        "   -p             Turn off power after shutdown, if supported\n"
        "   -r             Reboot the system\n"
        "   -s             Shutdown the system\n";

/**
 Display usage text to the user.
 */
BOOL
ShutdownHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Shutdown %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strShutdownHelpText);
    return TRUE;
}

/**
 If the privilege for shutting down the system is available to the process,
 enable it.

 @return TRUE to indicate the privilege was enabled, FALSE if it was not.
 */
BOOL
ShutdownEnableShutdownPrivilege(VOID)
{
    TOKEN_PRIVILEGES TokenPrivileges;
    LUID ShutdownLuid;
    HANDLE TokenHandle;

    YoriLibLoadAdvApi32Functions();
    if (DllAdvApi32.pLookupPrivilegeValueW == NULL ||
        DllAdvApi32.pOpenProcessToken == NULL ||
        DllAdvApi32.pAdjustTokenPrivileges == NULL) {

        return FALSE;
    }

    if (!DllAdvApi32.pLookupPrivilegeValueW(NULL, SE_SHUTDOWN_NAME, &ShutdownLuid)) {
        return FALSE;
    }

    if (!DllAdvApi32.pOpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &TokenHandle)) {
        return FALSE;
    }

    TokenPrivileges.PrivilegeCount = 1;
    TokenPrivileges.Privileges[0].Luid = ShutdownLuid;
    TokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
    if (!DllAdvApi32.pAdjustTokenPrivileges(TokenHandle, FALSE, &TokenPrivileges, 0, NULL, 0)) {
        CloseHandle(TokenHandle);
        return FALSE;
    }
    CloseHandle(TokenHandle);

    return TRUE;
}

/**
 The set of operations supported by this command.
 */
typedef enum _SHUTDN_OP {
    ShutdownOpUsage = 0,
    ShutdownOpLogoff = 1,
    ShutdownOpShutdown = 2,
    ShutdownOpReboot = 3,
    ShutdownOpLock = 4
} SHUTDN_OP;

#ifdef YORI_BUILTIN
/**
 The main entrypoint for the shutdown builtin command.
 */
#define ENTRYPOINT YoriCmd_YSHUTDN
#else
/**
 The main entrypoint for the shutdown standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the shutdown cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the child process on success, or failure if the child
         could not be launched.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    BOOL Result;
    DWORD i;
    DWORD StartArg = 0;
    YORI_STRING Arg;
    SHUTDN_OP Op;
    LPTSTR ErrText;
    BOOL Force;
    BOOL Powerdown;
    DWORD ShutdownOptions;
    DWORD Err;

    Op = ShutdownOpUsage;
    Force = FALSE;
    Powerdown = FALSE;

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                ShutdownHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2019"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("f")) == 0) {
                Force = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("k")) == 0) {
                Op = ShutdownOpLock;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("l")) == 0) {
                Op = ShutdownOpLogoff;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("p")) == 0) {
                Powerdown = TRUE;
                Op = ShutdownOpShutdown;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("r")) == 0) {
                Op = ShutdownOpReboot;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("s")) == 0) {
                Op = ShutdownOpShutdown;
                ArgumentUnderstood = TRUE;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    YoriLibLoadAdvApi32Functions();
    YoriLibLoadUser32Functions();

    ShutdownEnableShutdownPrivilege();

    Result = TRUE;

    switch(Op) {
        case ShutdownOpUsage:
            ShutdownHelp();
            break;
        case ShutdownOpLogoff:
            if (DllUser32.pExitWindowsEx == NULL) {
                YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("shutdn: OS support not present\n"));
                return EXIT_FAILURE;
            }
            ShutdownOptions = EWX_LOGOFF;
            if (Force) {
                ShutdownOptions = ShutdownOptions | EWX_FORCE;
            }
            Result = DllUser32.pExitWindowsEx(ShutdownOptions, 0);
            if (!Result) {
                ErrText = YoriLibGetWinErrorText(GetLastError());
                YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Logoff failed: %s"), ErrText);
                YoriLibFreeWinErrorText(ErrText);
            }
            break;
        case ShutdownOpReboot:
            if (DllUser32.pExitWindowsEx == NULL && DllAdvApi32.pInitiateShutdownW == NULL) {
                YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("shutdn: OS support not present\n"));
                return EXIT_FAILURE;
            }
            Err = ERROR_SUCCESS;
            if (DllUser32.pExitWindowsEx != NULL) {
                ShutdownOptions = EWX_REBOOT;
                if (Force) {
                    ShutdownOptions = ShutdownOptions | EWX_FORCE;
                }
                Result = DllUser32.pExitWindowsEx(ShutdownOptions, 0);
                if (!Result) {
                    Err = GetLastError();
                }
            } else {
                ShutdownOptions = SHUTDOWN_RESTART;
                if (Force) {
                    ShutdownOptions = ShutdownOptions | SHUTDOWN_FORCE_OTHERS | SHUTDOWN_FORCE_SELF;
                }
                Err = DllAdvApi32.pInitiateShutdownW(NULL, NULL, 0, ShutdownOptions, 0);
            }
            if (Err != ERROR_SUCCESS) {
                ErrText = YoriLibGetWinErrorText(Err);
                YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Reboot failed: %s"), ErrText);
                YoriLibFreeWinErrorText(ErrText);
            }
            break;
        case ShutdownOpShutdown:
            if (DllUser32.pExitWindowsEx == NULL && DllAdvApi32.pInitiateShutdownW == NULL) {
                YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("shutdn: OS support not present\n"));
                return EXIT_FAILURE;
            }
            Err = ERROR_SUCCESS;
            if (DllUser32.pExitWindowsEx != NULL) {
                ShutdownOptions = EWX_SHUTDOWN;
                if (Force) {
                    ShutdownOptions = ShutdownOptions | EWX_FORCE;
                }
                if (Powerdown) {
                    ShutdownOptions = ShutdownOptions | EWX_POWEROFF;
                }
                if (!DllUser32.pExitWindowsEx(ShutdownOptions, 0)) {
                    Err = GetLastError();
                }
            } else {
                ShutdownOptions = SHUTDOWN_NOREBOOT;
                if (Powerdown) {
                    ShutdownOptions = SHUTDOWN_POWEROFF;
                }

                if (Force) {
                    ShutdownOptions = ShutdownOptions | SHUTDOWN_FORCE_OTHERS | SHUTDOWN_FORCE_SELF;
                }
                Err = DllAdvApi32.pInitiateShutdownW(NULL, NULL, 0, ShutdownOptions, 0);
            }
            if (!Result) {
                ErrText = YoriLibGetWinErrorText(Err);
                YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Shutdown failed: %s"), ErrText);
                YoriLibFreeWinErrorText(ErrText);
            }
            break;
        case ShutdownOpLock:
            if (DllUser32.pLockWorkStation == NULL) {
                YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("shutdn: OS support not present\n"));
                return EXIT_FAILURE;
            }
            DllUser32.pLockWorkStation();
            break;
    }

    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>sponge/sponge.c<|end_filename|>
/**
 * @file sponge/sponge.c
 *
 * Yori shell filter within a line of output
 *
 * Copyright (c) 2019 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 Help text to display to the user.
 */
const
CHAR strSpongeHelpText[] =
        "\n"
        "Read input into memory and output once all input is read,\n"
        "  allowing the output to modify the source stream.\n"
        "\n"
        "SPONGE [-license] [file]\n"
        ;

/**
 Display usage text to the user.
 */
BOOL
SpongeHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Sponge %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strSpongeHelpText);
    return TRUE;
}

/**
 A buffer for a single data stream.
 */
typedef struct _SPONGE_BUFFER {

    /**
     The number of bytes currently allocated to this buffer.
     */
    DWORD BytesAllocated;

    /**
     The number of bytes populated with data in this buffer.
     */
    DWORD BytesPopulated;

    /**
     A handle to a pipe which is the source of data for this buffer.
     */
    HANDLE hSource;

    /**
     The data buffer.
     */
    PCHAR Buffer;

} SPONGE_BUFFER, *PSPONGE_BUFFER;

/**
 Populate data from stdin into an in memory buffer.

 @param ThisBuffer A pointer to the process buffer set.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SpongeBufferPump(
    __in PSPONGE_BUFFER ThisBuffer
    )
{
    DWORD BytesRead;
    BOOL Result = FALSE;

    while (TRUE) {

        if (ReadFile(ThisBuffer->hSource,
                     YoriLibAddToPointer(ThisBuffer->Buffer, ThisBuffer->BytesPopulated),
                     ThisBuffer->BytesAllocated - ThisBuffer->BytesPopulated,
                     &BytesRead,
                     NULL)) {

            if (BytesRead == 0) {
                Result = TRUE;
                break;
            }

            ThisBuffer->BytesPopulated += BytesRead;
            ASSERT(ThisBuffer->BytesPopulated <= ThisBuffer->BytesAllocated);
            if (ThisBuffer->BytesPopulated >= ThisBuffer->BytesAllocated) {
                DWORD NewBytesAllocated;
                PCHAR NewBuffer;

                if (ThisBuffer->BytesAllocated >= ((DWORD)-1) / 4) {
                    break;
                }

                NewBytesAllocated = ThisBuffer->BytesAllocated * 4;

                NewBuffer = YoriLibMalloc(NewBytesAllocated);
                if (NewBuffer == NULL) {
                    break;
                }

                memcpy(NewBuffer, ThisBuffer->Buffer, ThisBuffer->BytesAllocated);
                YoriLibFree(ThisBuffer->Buffer);
                ThisBuffer->Buffer = NewBuffer;
                ThisBuffer->BytesAllocated = NewBytesAllocated;
            }
        } else {
            Result = TRUE;
            break;
        }
    }

    return Result;
}

/**
 Output the collected buffer to a stream.

 @param ThisBuffer Pointer to the buffer to output.

 @param hTarget Handle to the target stream to output the buffer to.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SpongeBufferForward(
    __in PSPONGE_BUFFER ThisBuffer,
    __in HANDLE hTarget
    )
{
    DWORD BytesSent;
    BOOL Result;

    BytesSent = 0;
    Result = TRUE;

    while (BytesSent < ThisBuffer->BytesPopulated) {
        DWORD BytesToWrite;
        DWORD BytesWritten;
        BytesToWrite = 4096;
        if (BytesSent + BytesToWrite > ThisBuffer->BytesPopulated) {
            BytesToWrite = ThisBuffer->BytesPopulated - BytesSent;
        }

        if (WriteFile(hTarget,
                      YoriLibAddToPointer(ThisBuffer->Buffer, BytesSent),
                      BytesToWrite,
                      &BytesWritten,
                      NULL)) {

            BytesSent += BytesWritten;
        } else {
            Result = FALSE;
        }

        ASSERT(BytesSent <= ThisBuffer->BytesPopulated);
    }

    return Result;
}

/**
 Allocate and initialize a buffer for an input stream.

 @param Buffer Pointer to the buffer to allocate structures for.

 @return TRUE if the buffer is successfully initialized, FALSE if it is not.
 */
BOOL
SpongeAllocateBuffer(
    __out PSPONGE_BUFFER Buffer
    )
{
    Buffer->BytesAllocated = 1024;
    Buffer->Buffer = YoriLibMalloc(Buffer->BytesAllocated);
    if (Buffer->Buffer == NULL) {
        return FALSE;
    }

    return TRUE;
}

/**
 Free structures associated with a single input stream.

 @param ThisBuffer Pointer to the single stream's buffers to deallocate.
 */
VOID
SpongeFreeBuffer(
    __in PSPONGE_BUFFER ThisBuffer
    )
{
    if (ThisBuffer->Buffer != NULL) {
        YoriLibFree(ThisBuffer->Buffer);
    }
}


#ifdef YORI_BUILTIN
/**
 The main entrypoint for the sponge builtin command.
 */
#define ENTRYPOINT YoriCmd_YSPONGE
#else
/**
 The main entrypoint for the sponge standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the sponge cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of zero to indicate success, nonzero to indicate failure.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    YORI_STRING Arg;
    SPONGE_BUFFER SpongeBuffer;
    YORI_STRING FullFilePath;
    HANDLE hTarget;

    ZeroMemory(&SpongeBuffer, sizeof(SpongeBuffer));

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                SpongeHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2019"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                ArgumentUnderstood = TRUE;
                StartArg = i + 1;
                break;
            }
        } else {
            StartArg = i;
            ArgumentUnderstood = TRUE;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }


#if YORI_BUILTIN
    YoriLibCancelEnable();
#endif

    if (GetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), &i)) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("No file or pipe for input\n"));
        return EXIT_FAILURE;
    }

    if (!SpongeAllocateBuffer(&SpongeBuffer)) {
        return EXIT_FAILURE;
    }
    SpongeBuffer.hSource = GetStdHandle(STD_INPUT_HANDLE);

    YoriLibInitEmptyString(&FullFilePath);
    hTarget = GetStdHandle(STD_OUTPUT_HANDLE);
    if (StartArg != 0 && StartArg < ArgC) {
        YoriLibInitEmptyString(&FullFilePath);
        if (!YoriLibUserStringToSingleFilePath(&ArgV[StartArg], TRUE, &FullFilePath)) {
            SpongeFreeBuffer(&SpongeBuffer);
            return EXIT_FAILURE;
        }
    }

    if (!SpongeBufferPump(&SpongeBuffer)) {
        SpongeFreeBuffer(&SpongeBuffer);
        return EXIT_FAILURE;
    }

    if (FullFilePath.LengthInChars > 0) {
        hTarget = CreateFile(FullFilePath.StartOfString,
                             GENERIC_WRITE,
                             FILE_SHARE_READ | FILE_SHARE_DELETE,
                             NULL,
                             CREATE_ALWAYS,
                             0,
                             NULL);
        if (hTarget == INVALID_HANDLE_VALUE) {
            DWORD LastError = GetLastError();
            LPTSTR ErrText = YoriLibGetWinErrorText(LastError);
            SpongeFreeBuffer(&SpongeBuffer);
            YoriLibFreeStringContents(&FullFilePath);
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("sponge: open file failed: %s"), ErrText);
            YoriLibFreeWinErrorText(ErrText);
            return EXIT_FAILURE;
        }
    }

    SpongeBufferForward(&SpongeBuffer, hTarget);

    if (FullFilePath.LengthInChars > 0) {
        CloseHandle(hTarget);
        YoriLibFreeStringContents(&FullFilePath);
    }

    SpongeFreeBuffer(&SpongeBuffer);

    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>builtins/set.c<|end_filename|>
/**
 * @file builtins/set.c
 *
 * Yori shell environment variable control
 *
 * Copyright (c) 2017-2019 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>
#include <yoricall.h>

/**
 Help text to display to the user.
 */
const
CHAR strSetHelpText[] =
        "\n"
        "Displays or updates environment variables.\n"
        "\n"
        "SET -license\n"
        "SET [<variable prefix to display>]\n"
        "SET [-e | -i | -r] <variable>=<value>\n"
        "SET <variable to delete>=\n"
        "\n"
        "   -e             Include the string in a semicolon delimited variable at the end\n"
        "   -i             Include the string in a semicolon delimited variable at the start\n"
        "   -r             Remove the string from a semicolon delimited variable\n";

/**
 Display usage text to the user.
 */
BOOL
SetHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Set %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strSetHelpText);
    return TRUE;
}

/**
 Perform an in place removal of escape characters.  This function assumes the
 string is NULL terminated and will leave it NULL terminated on completion.

 @param String Pointer to the string to remove escape characters from.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SetRemoveEscapes(
    __inout PYORI_STRING String
    )
{
    DWORD CharIndex;
    DWORD DestIndex;

    for (CharIndex = 0, DestIndex = 0; CharIndex < String->LengthInChars; CharIndex++, DestIndex++) {
        if (YoriLibIsEscapeChar(String->StartOfString[CharIndex])) {
            CharIndex++;
            if (CharIndex >= String->LengthInChars) {
                break;
            }
        }

        if (CharIndex != DestIndex) {
            String->StartOfString[DestIndex] = String->StartOfString[CharIndex];
        }
    }
    String->StartOfString[DestIndex] = '\0';
    String->LengthInChars = DestIndex;
    return TRUE;
}

/**
 Main entrypoint for the set builtin.

 @param ArgC The number of arguments.

 @param ArgV An array of string arguments.

 @return Exit code, typically zero for success and nonzero for failure.
 */
DWORD
YORI_BUILTIN_FN
YoriCmd_SET(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    BOOL AppendComponent = FALSE;
    BOOL IncludeComponent = FALSE;
    BOOL RemoveComponent = FALSE;
    DWORD i;
    DWORD StartArg = 0;
    YORI_STRING Arg;

    DWORD EscapedArgC;
    PYORI_STRING EscapedArgV;

    YoriLibLoadNtDllFunctions();
    YoriLibLoadKernel32Functions();

    if (!YoriCallGetEscapedArguments(&EscapedArgC, &EscapedArgV)) {
        EscapedArgC = ArgC;
        EscapedArgV = ArgV;
    }

    for (i = 1; i < EscapedArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&EscapedArgV[i]));

        if (YoriLibIsCommandLineOption(&EscapedArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                SetHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2017-2019"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("e")) == 0) {
                if (!RemoveComponent && !IncludeComponent) {
                    AppendComponent = TRUE;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("i")) == 0) {
                if (!RemoveComponent && !AppendComponent) {
                    IncludeComponent = TRUE;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("r")) == 0) {
                if (!IncludeComponent && !AppendComponent) {
                    RemoveComponent = TRUE;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                StartArg = i;
                ArgumentUnderstood = TRUE;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &EscapedArgV[i]);
        }
    }

    if (StartArg == 0) {
        YORI_STRING EnvironmentStrings;
        LPTSTR ThisVar;
        DWORD VarLen;

        if (!YoriLibGetEnvironmentStrings(&EnvironmentStrings)) {
            return EXIT_FAILURE;
        }
        ThisVar = EnvironmentStrings.StartOfString;
        while (*ThisVar != '\0') {
            VarLen = _tcslen(ThisVar);
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%s\n"), ThisVar);
            ThisVar += VarLen;
            ThisVar++;
        }
        YoriLibFreeStringContents(&EnvironmentStrings);
    } else {
        YORI_STRING Value;
        YORI_STRING CmdLine;
        YORI_STRING Variable;

        if (!YoriLibBuildCmdlineFromArgcArgv(EscapedArgC - StartArg, &EscapedArgV[StartArg], FALSE, FALSE, &CmdLine)) {
            return EXIT_FAILURE;
        }

        memcpy(&Variable, &CmdLine, sizeof(YORI_STRING));

        //
        //  At this point escapes are still present but it's never valid to
        //  have an '=' in a variable name, escape or not.
        //

        YoriLibInitEmptyString(&Value);
        Value.StartOfString = YoriLibFindLeftMostCharacter(&Variable, '=');
        if (Value.StartOfString) {
            Value.StartOfString[0] = '\0';
            Value.StartOfString++;
            Variable.LengthAllocated = (DWORD)(Value.StartOfString - CmdLine.StartOfString);
            Variable.LengthInChars = Variable.LengthAllocated - 1;
            Value.LengthInChars = CmdLine.LengthInChars - Variable.LengthAllocated;
            Value.LengthAllocated = CmdLine.LengthAllocated - Variable.LengthAllocated;
        }

        SetRemoveEscapes(&Variable);

        if (Value.StartOfString != NULL) {

            //
            //  Scan through the value looking for any unexpanded environment
            //  variables.
            //

            YoriLibBuiltinRemoveEmptyVariables(&Value);
            Value.StartOfString[Value.LengthInChars] = '\0';

            if (Value.LengthInChars == 0) {
                YoriCallSetEnvironmentVariable(&Variable, NULL);
            } else {
                YORI_STRING CombinedValue;
                SetRemoveEscapes(&Value);
                if (IncludeComponent) {
                    if (YoriLibAddEnvironmentComponentReturnString(&Variable, &Value, TRUE, &CombinedValue)) {
                        YoriCallSetEnvironmentVariable(&Variable, &CombinedValue);
                        YoriLibFreeStringContents(&CombinedValue);
                    }
                } else if (AppendComponent) {
                    if (YoriLibAddEnvironmentComponentReturnString(&Variable, &Value, FALSE, &CombinedValue)) {
                        YoriCallSetEnvironmentVariable(&Variable, &CombinedValue);
                        YoriLibFreeStringContents(&CombinedValue);
                    }
                } else if (RemoveComponent) {
                    if (YoriLibRemoveEnvironmentComponentReturnString(&Variable, &Value, &CombinedValue)) {
                        if (CombinedValue.LengthAllocated > 0) {
                            YoriCallSetEnvironmentVariable(&Variable, &CombinedValue);
                        } else {
                            YoriCallSetEnvironmentVariable(&Variable, NULL);
                        }
                        YoriLibFreeStringContents(&CombinedValue);
                    }
                } else {
                    YoriCallSetEnvironmentVariable(&Variable, &Value);
                }
            }
        } else {
            YORI_STRING EnvironmentStrings;
            LPTSTR ThisVar;

            if (!YoriLibGetEnvironmentStrings(&EnvironmentStrings)) {
                YoriLibFreeStringContents(&CmdLine);
                return EXIT_FAILURE;
            }
            ThisVar = EnvironmentStrings.StartOfString;
            while (*ThisVar != '\0') {
                if (YoriLibCompareStringWithLiteralInsensitiveCount(&Variable, ThisVar, Variable.LengthInChars) == 0) {
                    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%s\n"), ThisVar);
                }
                ThisVar += _tcslen(ThisVar);
                ThisVar++;
            }
            YoriLibFreeStringContents(&EnvironmentStrings);
        }
        YoriLibFreeStringContents(&CmdLine);
    }
    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>cpuinfo/cpuinfo.c<|end_filename|>
/**
 * @file cpuinfo/cpuinfo.c
 *
 * Yori shell display cpu topology information
 *
 * Copyright (c) 2019-2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>
#include <winperf.h>

/**
 Help text to display to the user.
 */
const
CHAR strCpuInfoHelpText[] =
        "\n"
        "Display cpu topology information.\n"
        "\n"
        "CPUINFO [-license] [-a] [-c] [-g] [-n] [-s] [-w ms] [<fmt>]\n"
        "\n"
        "   -a             Display all information\n"
        "   -c             Display information about processor cores\n"
        "   -g             Display information about processor groups\n"
        "   -n             Display information about NUMA nodes\n"
        "   -s             Display information about processor sockets\n"
        "   -w ms          Wait time to measure CPU utilization\n"
        "\n"
        "Format specifiers are:\n"
        "   $CORECOUNT$            The number of processor cores\n"
        "   $GROUPCOUNT$           The number of processor groups\n"
        "   $LOGICALCOUNT$         The number of logical processors\n"
        "   $NUMANODECOUNT$        The number of NUMA nodes\n"
        "   $UTILIZATION$          The current CPU utilization\n";

/**
 Display usage text to the user.
 */
BOOL
CpuInfoHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("CpuInfo %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strCpuInfoHelpText);
    return TRUE;
}

/**
 Output a 64 bit integer.

 @param LargeInt A large integer to output.

 @param NumberBase Specifies the numeric base to use.  Should be 10 for
        decimal or 16 for hex.

 @param OutputString Pointer to a string to populate with the contents of
        the variable.

 @return The number of characters populated into the variable, or the number
         of characters required to successfully populate the contents into
         the variable.
 */
DWORD
CpuInfoOutputLargeInteger(
    __in LARGE_INTEGER LargeInt,
    __in DWORD NumberBase,
    __inout PYORI_STRING OutputString
    )
{
    YORI_STRING String;
    TCHAR StringBuffer[32];

    YoriLibInitEmptyString(&String);
    String.StartOfString = StringBuffer;
    String.LengthAllocated = sizeof(StringBuffer)/sizeof(StringBuffer[0]);

    YoriLibNumberToString(&String, LargeInt.QuadPart, NumberBase, 0, ' ');

    if (OutputString->LengthAllocated >= String.LengthInChars) {
        memcpy(OutputString->StartOfString, String.StartOfString, String.LengthInChars * sizeof(TCHAR));
    }

    return String.LengthInChars;
}

/**
 Context about cpuinfoory state that is passed between cpuinfoory query and string
 expansion.
 */
typedef struct _CPUINFO_CONTEXT {

    /**
     The number of bytes in the ProcInfo allocation.
     */
    DWORD BytesInBuffer;

    /**
     The time to wait when measuring CPU utilization.
     */
    DWORD WaitTime;

    /**
     The CPU utilization in hundredths of a percent.
     */
    DWORD Utilization;

    /**
     TRUE if the CPU utilization has already been loaded.
     */
    BOOLEAN UtilizationLoaded;

    /**
     TRUE if the CPU topological information has already been loaded.
     */
    BOOLEAN TopologyLoaded;

    /**
     An array of entries describing information about the current system.
     */
    PYORI_SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX ProcInfo;

    /**
     The total number of processor cores discovered in the above information.
     */
    LARGE_INTEGER CoreCount;

    /**
     The total number of logical processors discovered in the above
     information.
     */
    LARGE_INTEGER LogicalProcessorCount;

    /**
     The total number of NUMA nodes discovered in the above information.
     */
    LARGE_INTEGER NumaNodeCount;

    /**
     The total number of processor groups discovered in the above information.
     */
    LARGE_INTEGER GroupCount;

} CPUINFO_CONTEXT, *PCPUINFO_CONTEXT;

/**
 Scan through the performance counter information looking for counter "6"
 which is CPU utilization, on instance "_Total" for all processors.  If
 _Total is not available, just take the first processor.

 @param PerfData Pointer to the performance counter information.

 @param IdleTime On successful completion, updated to contain the amount of
        time the processor has been idle since system start.

 @return TRUE to indicate success, FALSE to indicate that the coutner was not
         found.
 */
__success(return)
BOOLEAN
CpuInfoCaptureProcessorIdleTime(
    __in PPERF_DATA_BLOCK PerfData,
    __out PDWORDLONG IdleTime
    )
{
    PPERF_OBJECT_TYPE PerfObject;
    PPERF_INSTANCE_DEFINITION PerfInstance;
    PPERF_COUNTER_DEFINITION PerfCounter;
    PPERF_COUNTER_BLOCK PerfBlock;
    DWORD PerfDataObjectIndex;
    DWORD TargetInstance;
    DWORD CounterIndex;
    YORI_STRING InstanceString;

    YoriLibInitEmptyString(&InstanceString);

    PerfObject = YoriLibAddToPointer(PerfData, PerfData->HeaderLength);
    for (PerfDataObjectIndex = 0; PerfDataObjectIndex < PerfData->NumObjectTypes; PerfDataObjectIndex++) {

        PerfCounter = YoriLibAddToPointer(PerfObject, PerfObject->HeaderLength);

        //
        //  The CPU counter has instances.
        //

        if (PerfObject->NumInstances > 0) {

            PerfInstance = YoriLibAddToPointer(PerfObject, PerfObject->DefinitionLength);
            PerfBlock = YoriLibAddToPointer(PerfInstance, PerfInstance->ByteLength);

            //
            //  Look for an instance called "_Total"
            //

            for (TargetInstance = 0; TargetInstance < (DWORD)PerfObject->NumInstances; TargetInstance++) {

                InstanceString.StartOfString = YoriLibAddToPointer(PerfInstance, PerfInstance->NameOffset);
                InstanceString.LengthInChars = PerfInstance->NameLength / sizeof(TCHAR);
                if (InstanceString.LengthInChars > 0) {
                    InstanceString.LengthInChars--;
                }

                if (YoriLibCompareStringWithLiteralInsensitive(&InstanceString, _T("_Total")) == 0) {
                    break;
                }

                PerfBlock = YoriLibAddToPointer(PerfInstance, PerfInstance->ByteLength);
                PerfInstance = YoriLibAddToPointer(PerfBlock, PerfBlock->ByteLength);
            }

            //
            //  Failing that, older versions of the OS don't include _Total if
            //  there's only one processor, so just take the first
            //

            if (TargetInstance == (DWORD)PerfObject->NumInstances) {
                TargetInstance = 0;
                PerfInstance = YoriLibAddToPointer(PerfObject, PerfObject->DefinitionLength);
            }

            //
            //  Within that instance, look for a counter with a value of 6,
            //  being processor utilization
            //

            PerfCounter = YoriLibAddToPointer(PerfObject, PerfObject->HeaderLength);
            for (CounterIndex = 0; CounterIndex < PerfObject->NumCounters; CounterIndex++) {
                if (PerfCounter->CounterNameTitleIndex == 6 &&
                    PerfCounter->CounterSize == sizeof(DWORDLONG)) {
                    PDWORDLONG Value;

                    Value = YoriLibAddToPointer(PerfBlock, PerfCounter->CounterOffset);
                    *IdleTime = *Value;
                    return TRUE;
                }
                PerfCounter = YoriLibAddToPointer(PerfCounter, PerfCounter->ByteLength);
            }
        }
        PerfObject = YoriLibAddToPointer(PerfObject, PerfObject->TotalByteLength);
    }

    return FALSE;
}

/**
 Determine the amount of processor utilization.  Note this requires sleeping
 for a period of time, so it is deferred until it is required.

 @param CpuInfoContext Pointer to the CpuInfo context to populate with
        processor utilization information.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOLEAN
CpuInfoLoadProcessorUtilization(
    __inout PCPUINFO_CONTEXT CpuInfoContext
    )
{
    DWORD BufferSize;
    DWORD BufferPopulated;
    PPERF_DATA_BLOCK PerfData;
    DWORD TimeIndex;
    DWORD Err;
    LARGE_INTEGER TimeValue[2];
    DWORDLONG IdleValue[2];

    UNREFERENCED_PARAMETER(CpuInfoContext);

    YoriLibLoadAdvApi32Functions();

    if (DllAdvApi32.pRegQueryValueExW == NULL) {
        return FALSE;
    }

    BufferSize = 64 * 1024;
    PerfData = YoriLibMalloc(BufferSize);
    if (PerfData == NULL) {
        return FALSE;
    }

    for (TimeIndex = 0; TimeIndex < 2; TimeIndex++) {
        Err = ERROR_SUCCESS;

        //
        //  Query the registry, resizing the buffer if it's too small.
        //

        while (TRUE) {
            BufferPopulated = BufferSize;
            Err = DllAdvApi32.pRegQueryValueExW(HKEY_PERFORMANCE_DATA, _T("238"), NULL, NULL, (LPBYTE)PerfData, &BufferPopulated);
            if (Err != ERROR_MORE_DATA) {
                break;
            }

            if (BufferSize <= 16 * 1024 * 1024) {
                BufferSize = BufferSize * 4;
            }

            YoriLibFree(PerfData);
            PerfData = YoriLibMalloc(BufferSize);
            if (PerfData == NULL) {
                return FALSE;
            }
        }

        if (Err != ERROR_SUCCESS) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("RegQueryValueExW failed %i\n"), Err);
            YoriLibFree(PerfData);
            return FALSE;
        }

        TimeValue[TimeIndex].QuadPart = PerfData->PerfTime100nSec.QuadPart;
        if (!CpuInfoCaptureProcessorIdleTime(PerfData, &IdleValue[TimeIndex])) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Cannot find idle time in performance data\n"));
            YoriLibFree(PerfData);
            return FALSE;
        }

        Sleep(CpuInfoContext->WaitTime);
    }
    YoriLibFree(PerfData);

    {
        DWORDLONG TimeDelta;
        DWORDLONG IdleDelta;
        DWORD BusyPercent;
        DWORD IdlePercent;

        TimeDelta = TimeValue[1].QuadPart - TimeValue[0].QuadPart;
        IdleDelta = IdleValue[1] - IdleValue[0];

        if (IdleDelta > TimeDelta) {
            IdleDelta = TimeDelta;
        }
        IdlePercent = (DWORD)(IdleDelta * 10000 / TimeDelta);
        BusyPercent = 10000 - IdlePercent;

        CpuInfoContext->Utilization = BusyPercent;
        CpuInfoContext->UtilizationLoaded = TRUE;
    }

    return TRUE;
}


/**
 A callback function to expand any known variables found when parsing the
 format string.

 @param OutputBuffer A pointer to the output buffer to populate with data
        if a known variable is found.

 @param VariableName The variable name to expand.

 @param Context Pointer to a SYSTEMTIME structure containing the data to
        populate.
 
 @return The number of characters successfully populated, or the number of
         characters required in order to successfully populate, or zero
         on error.
 */
DWORD
CpuInfoExpandVariables(
    __inout PYORI_STRING OutputBuffer,
    __in PYORI_STRING VariableName,
    __in PVOID Context
    )
{
    DWORD CharsNeeded;
    PCPUINFO_CONTEXT CpuInfoContext = (PCPUINFO_CONTEXT)Context;

    if (YoriLibCompareStringWithLiteral(VariableName, _T("CORECOUNT")) == 0) {
        return CpuInfoOutputLargeInteger(CpuInfoContext->CoreCount, 10, OutputBuffer);
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("GROUPCOUNT")) == 0) {
        return CpuInfoOutputLargeInteger(CpuInfoContext->GroupCount, 10, OutputBuffer);
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("LOGICALCOUNT")) == 0) {
        return CpuInfoOutputLargeInteger(CpuInfoContext->LogicalProcessorCount, 10, OutputBuffer);
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("NUMANODECOUNT")) == 0) {
        return CpuInfoOutputLargeInteger(CpuInfoContext->NumaNodeCount, 10, OutputBuffer);
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("UTILIZATION")) == 0) {
        if (!CpuInfoContext->UtilizationLoaded) {
            CpuInfoLoadProcessorUtilization(CpuInfoContext);
        }
        CharsNeeded = 7;
        if (OutputBuffer->LengthAllocated > CharsNeeded) {
            CharsNeeded = YoriLibSPrintf(OutputBuffer->StartOfString, _T("%i.%02i%%"), CpuInfoContext->Utilization / 100, CpuInfoContext->Utilization % 100);
        }
    } else {
        return 0;
    }

    return CharsNeeded;
}

//
//  The CPUINFO_CONTEXT structure records a pointer to an array and the size
//  of the array in bytes.  Unfortunately I can't see a way to describe this
//  relationship for older analyzers, so they believe accessing array elements
//  is walking off the end of the buffer.  Because this seems specific to
//  older versions, the suppression is limited to those also.
//

#if defined(_MSC_VER) && (_MSC_VER >= 1500) && (_MSC_VER <= 1600)
#pragma warning(push)
#pragma warning(disable: 6385)
#endif

/**
 Parse the array of information about processor topologies and count the
 number of elements in each.

 @param CpuInfoContext Pointer to the context which describes the processor
        information verbosely and should have summary counts populated.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
CpuInfoCountSummaries(
    __inout PCPUINFO_CONTEXT CpuInfoContext
    )
{
    PYORI_SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX Entry;
    DWORD CurrentOffset = 0;
    DWORD GroupIndex;
    DWORD LogicalProcessorIndex;
    DWORD_PTR LogicalProcessorMask;
    PYORI_PROCESSOR_GROUP_AFFINITY Group;

    Entry = CpuInfoContext->ProcInfo;

    while (Entry != NULL) {

        if (Entry->Relationship == YoriProcessorRelationProcessorCore) {
            CpuInfoContext->CoreCount.QuadPart++;
            for (GroupIndex = 0; GroupIndex < Entry->u.Processor.GroupCount; GroupIndex++) {
                Group = &Entry->u.Processor.GroupMask[GroupIndex];
                for (LogicalProcessorIndex = 0; LogicalProcessorIndex < 8 * sizeof(DWORD_PTR); LogicalProcessorIndex++) {
                    LogicalProcessorMask = 1;
                    LogicalProcessorMask = LogicalProcessorMask<<LogicalProcessorIndex;
                    if (Group->Mask & LogicalProcessorMask) {
                        CpuInfoContext->LogicalProcessorCount.QuadPart++;
                    }
                }
            }
        } else if (Entry->Relationship == YoriProcessorRelationNumaNode) {
            CpuInfoContext->NumaNodeCount.QuadPart++;
        } else if (Entry->Relationship == YoriProcessorRelationGroup) {
            CpuInfoContext->GroupCount.QuadPart = Entry->u.Group.ActiveGroupCount;
        }

        CurrentOffset += Entry->SizeInBytes;
        if (CurrentOffset >= CpuInfoContext->BytesInBuffer) {
            break;
        }
        Entry = YoriLibAddToPointer(CpuInfoContext->ProcInfo, CurrentOffset);
    }

    return TRUE;
}

/**
 Display a list of logical processor numbers.

 @param GroupIndex The group that contains these logical processors.

 @param Processors A bitmask of processors.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
CpuInfoDisplayProcessorMask(
    __in WORD GroupIndex,
    __in DWORD_PTR Processors
    )
{
    DWORD LogicalProcessorIndex;
    DWORD_PTR LogicalProcessorMask;

    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Processors: "));

    for (LogicalProcessorIndex = 0; LogicalProcessorIndex < 8 * sizeof(DWORD_PTR); LogicalProcessorIndex++) {
        LogicalProcessorMask = 1;
        LogicalProcessorMask = LogicalProcessorMask<<LogicalProcessorIndex;
        if (Processors & LogicalProcessorMask) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%i "), GroupIndex * 64 + LogicalProcessorIndex);
        }
    }
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("\n"));

    return TRUE;
}

/**
 Display processors within each processor core.

 @param CpuInfoContext Pointer to the context containing processor layout.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
CpuInfoDisplayCores(
    __in PCPUINFO_CONTEXT CpuInfoContext
    )
{
    PYORI_SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX Entry;
    DWORD CurrentOffset = 0;
    DWORD CoreIndex = 0;
    DWORD GroupIndex;

    Entry = CpuInfoContext->ProcInfo;

    while (Entry != NULL) {

        if (Entry->Relationship == YoriProcessorRelationProcessorCore) {
            if (CoreIndex > 0) {
                YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("\n"));
            }
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Core %i\n"), CoreIndex);
            CoreIndex++;
            for (GroupIndex = 0; GroupIndex < Entry->u.Processor.GroupCount; GroupIndex++) {
                CpuInfoDisplayProcessorMask(Entry->u.Processor.GroupMask[GroupIndex].Group, Entry->u.Processor.GroupMask[GroupIndex].Mask);
            }
        }

        CurrentOffset += Entry->SizeInBytes;
        if (CurrentOffset >= CpuInfoContext->BytesInBuffer) {
            break;
        }
        Entry = YoriLibAddToPointer(CpuInfoContext->ProcInfo, CurrentOffset);
    }

    return TRUE;
}

/**
 Display processors within each processor group.

 @param CpuInfoContext Pointer to the context containing processor layout.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
CpuInfoDisplayGroups(
    __in PCPUINFO_CONTEXT CpuInfoContext
    )
{
    PYORI_SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX Entry;
    DWORD CurrentOffset = 0;
    WORD GroupIndex;
    BOOLEAN DisplayedGroup = FALSE;

    Entry = CpuInfoContext->ProcInfo;

    while (Entry != NULL) {

        if (Entry->Relationship == YoriProcessorRelationGroup) {
            if (DisplayedGroup) {
                YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("\n"));
            }

            for (GroupIndex = 0; GroupIndex < Entry->u.Group.MaximumGroupCount; GroupIndex++) {
                YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Group %i\n"), GroupIndex);
                CpuInfoDisplayProcessorMask(GroupIndex, Entry->u.Group.GroupInfo[GroupIndex].ActiveProcessorMask);
                DisplayedGroup = TRUE;
            }
        }

        CurrentOffset += Entry->SizeInBytes;
        if (CurrentOffset >= CpuInfoContext->BytesInBuffer) {
            break;
        }
        Entry = YoriLibAddToPointer(CpuInfoContext->ProcInfo, CurrentOffset);
    }

    return TRUE;
}

/**
 Display processors within each NUMA node.

 @param CpuInfoContext Pointer to the context containing processor layout.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
CpuInfoDisplayNuma(
    __in PCPUINFO_CONTEXT CpuInfoContext
    )
{
    PYORI_SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX Entry;
    DWORD CurrentOffset = 0;
    BOOLEAN DisplayedNode = FALSE;

    Entry = CpuInfoContext->ProcInfo;

    while (Entry != NULL) {

        if (Entry->Relationship == YoriProcessorRelationNumaNode) {
            if (DisplayedNode) {
                YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("\n"));
            }
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Numa Node %i\n"), Entry->u.NumaNode.NodeNumber);
            CpuInfoDisplayProcessorMask(Entry->u.NumaNode.GroupMask.Group, Entry->u.NumaNode.GroupMask.Mask);
            DisplayedNode = TRUE;
        }

        CurrentOffset += Entry->SizeInBytes;
        if (CurrentOffset >= CpuInfoContext->BytesInBuffer) {
            break;
        }
        Entry = YoriLibAddToPointer(CpuInfoContext->ProcInfo, CurrentOffset);
    }

    return TRUE;
}

/**
 Display processors within each processor package.

 @param CpuInfoContext Pointer to the context containing processor layout.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
CpuInfoDisplaySockets(
    __in PCPUINFO_CONTEXT CpuInfoContext
    )
{
    PYORI_SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX Entry;
    DWORD CurrentOffset = 0;
    DWORD SocketIndex = 0;
    DWORD GroupIndex;

    Entry = CpuInfoContext->ProcInfo;

    while (Entry != NULL) {

        if (Entry->Relationship == YoriProcessorRelationProcessorPackage) {
            if (SocketIndex > 0) {
                YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("\n"));
            }
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Socket %i\n"), SocketIndex);
            SocketIndex++;
            for (GroupIndex = 0; GroupIndex < Entry->u.Processor.GroupCount; GroupIndex++) {
                CpuInfoDisplayProcessorMask(Entry->u.Processor.GroupMask[GroupIndex].Group, Entry->u.Processor.GroupMask[GroupIndex].Mask);
            }
        }

        CurrentOffset += Entry->SizeInBytes;
        if (CurrentOffset >= CpuInfoContext->BytesInBuffer) {
            break;
        }
        Entry = YoriLibAddToPointer(CpuInfoContext->ProcInfo, CurrentOffset);
    }

    return TRUE;
}

#if defined(_MSC_VER) && (_MSC_VER >= 1500) && (_MSC_VER <= 1600)
#pragma warning(pop)
#endif

/**
 Load processor information from GetLogicalProcessorInformationEx.

 @param CpuInfoContext Pointer to the context to populate with processor
        information.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
CpuInfoLoadProcessorInfo(
    __inout PCPUINFO_CONTEXT CpuInfoContext
    )
{
    DWORD Err;

    //
    //  Query processor information from the system.  This needs to allocate
    //  memory as needed to populate, so loop while the buffer is too small
    //  in order to allocate the correct amount.
    //

    Err = ERROR_SUCCESS;
    while(TRUE) {
        if (DllKernel32.pGetLogicalProcessorInformationEx(YoriProcessorRelationAll, CpuInfoContext->ProcInfo, &CpuInfoContext->BytesInBuffer)) {
            Err = ERROR_SUCCESS;
            break;
        }

        Err = GetLastError();
        if (Err == ERROR_INSUFFICIENT_BUFFER) {
            if (CpuInfoContext->ProcInfo != NULL) {
                YoriLibFree(CpuInfoContext->ProcInfo);
                CpuInfoContext->ProcInfo = NULL;
            }

            CpuInfoContext->ProcInfo = YoriLibMalloc(CpuInfoContext->BytesInBuffer);
            if (CpuInfoContext->ProcInfo == NULL) {
                Err = ERROR_NOT_ENOUGH_MEMORY;
                break;
            }
        } else {
            break;
        }
    }

    //
    //  If the above query failed, indicate why.
    //

    if (Err != ERROR_SUCCESS) {
        LPTSTR ErrText;
        if (CpuInfoContext->ProcInfo != NULL) {
            YoriLibFree(CpuInfoContext->ProcInfo);
        }
        ErrText = YoriLibGetWinErrorText(Err);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Query failed: %s"), ErrText);
        YoriLibFreeWinErrorText(ErrText);
        return FALSE;
    }

    return TRUE;
}

/**
 Load processor information from GetLogicalProcessorInformation and translate
 the result into the output that would have come from
 GetLogicalProcessorInformationEx.  This is only done on systems without
 GetLogicalProcessorInformationEx, meaning they only have a single processor
 group.

 @param CpuInfoContext Pointer to the context to populate with processor
        information.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
CpuInfoLoadAndUpconvertProcessorInfo(
    __inout PCPUINFO_CONTEXT CpuInfoContext
    )
{
    DWORD Err;
    DWORD BytesInBuffer = 0;
    DWORD BytesRequired;
    DWORD CurrentOffset;
    DWORD NewOffset;
    DWORD_PTR ProcessorsFound;
    DWORD_PTR ProcessorMask;
    PYORI_SYSTEM_LOGICAL_PROCESSOR_INFORMATION ProcInfo = NULL;
    PYORI_SYSTEM_LOGICAL_PROCESSOR_INFORMATION Entry;
    PYORI_SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX NewEntry;

    //
    //  Query processor information from the system.  This needs to allocate
    //  memory as needed to populate, so loop while the buffer is too small
    //  in order to allocate the correct amount.
    //

    Err = ERROR_SUCCESS;
    while(TRUE) {
        if (DllKernel32.pGetLogicalProcessorInformation(ProcInfo, &BytesInBuffer)) {
            Err = ERROR_SUCCESS;
            break;
        }

        Err = GetLastError();
        if (Err == ERROR_INSUFFICIENT_BUFFER) {
            if (ProcInfo != NULL) {
                YoriLibFree(ProcInfo);
                ProcInfo = NULL;
            }

            ProcInfo = YoriLibMalloc(BytesInBuffer);
            if (ProcInfo == NULL) {
                Err = ERROR_NOT_ENOUGH_MEMORY;
                break;
            }
        } else {
            break;
        }
    }

    //
    //  If the above query failed, indicate why.
    //

    if (Err != ERROR_SUCCESS) {
        LPTSTR ErrText;
        if (ProcInfo != NULL) {
            YoriLibFree(ProcInfo);
        }
        ErrText = YoriLibGetWinErrorText(Err);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Query failed: %s"), ErrText);
        YoriLibFreeWinErrorText(ErrText);
        return FALSE;
    }

    if (ProcInfo == NULL) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("cpuinfo: no processors\n"));
        return FALSE;
    }

    //
    //  Count the amount of memory needed for the full structures.
    //

    BytesRequired = 0;
    CurrentOffset = 0;
    Entry = ProcInfo;

    while (Entry != NULL) {

        if (Entry->Relationship == YoriProcessorRelationProcessorCore) {
            BytesRequired += sizeof(YORI_SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX);
        } else if (Entry->Relationship == YoriProcessorRelationNumaNode) {
            BytesRequired += sizeof(YORI_SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX);
        } else if (Entry->Relationship == YoriProcessorRelationCache) {
            BytesRequired += sizeof(YORI_SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX);
        } else if (Entry->Relationship == YoriProcessorRelationProcessorPackage) {
            BytesRequired += sizeof(YORI_SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX);
        }

        CurrentOffset += sizeof(YORI_SYSTEM_LOGICAL_PROCESSOR_INFORMATION);
        if (CurrentOffset >= BytesInBuffer) {
            break;
        }
        Entry = YoriLibAddToPointer(ProcInfo, CurrentOffset);
    }

    //
    //  An extra one for the group information, which can only have one group.
    //

    BytesRequired += sizeof(YORI_SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX);

    CpuInfoContext->BytesInBuffer = BytesRequired;
    CpuInfoContext->ProcInfo = YoriLibMalloc(BytesRequired);
    if (CpuInfoContext->ProcInfo == NULL) {
        YoriLibFree(ProcInfo);
        return FALSE;
    }

    ZeroMemory(CpuInfoContext->ProcInfo, BytesRequired);

    //
    //  Port over the existing downlevel entries to the new format.
    //

    NewOffset = 0;
    CurrentOffset = 0;
    ProcessorsFound = 0;
    Entry = ProcInfo;
    NewEntry = CpuInfoContext->ProcInfo;

    //
    //  The first entry is for processor groups, but we don't know anything
    //  about them yet.  Reserve this entry and skip it.
    //

    NewEntry->Relationship = YoriProcessorRelationGroup;
    NewEntry->SizeInBytes = sizeof(YORI_SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX);
    NewEntry->u.Group.MaximumGroupCount = 1;
    NewEntry->u.Group.ActiveGroupCount = 1;
    NewOffset += NewEntry->SizeInBytes;
    NewEntry = YoriLibAddToPointer(CpuInfoContext->ProcInfo, NewOffset);

    while (Entry != NULL) {

        if (Entry->Relationship == YoriProcessorRelationProcessorCore) {
            NewEntry->Relationship = YoriProcessorRelationProcessorCore;
            NewEntry->SizeInBytes = sizeof(YORI_SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX);
            NewEntry->u.Processor.Flags = Entry->u.ProcessorCore.Flags;
            NewEntry->u.Processor.GroupCount = 1;
            NewEntry->u.Processor.GroupMask[0].Mask = Entry->ProcessorMask;
            ProcessorsFound |= Entry->ProcessorMask;
        } else if (Entry->Relationship == YoriProcessorRelationNumaNode) {
            NewEntry->Relationship = YoriProcessorRelationNumaNode;
            NewEntry->SizeInBytes = sizeof(YORI_SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX);
            NewEntry->u.NumaNode.NodeNumber = Entry->u.NumaNode.NodeNumber;
            NewEntry->u.NumaNode.GroupMask.Mask = Entry->ProcessorMask;

        } else if (Entry->Relationship == YoriProcessorRelationCache) {
            NewEntry->Relationship = YoriProcessorRelationCache;
            NewEntry->SizeInBytes = sizeof(YORI_SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX);
            memcpy(&NewEntry->u.Cache.Cache, &Entry->u.Cache, sizeof(YORI_PROCESSOR_CACHE_DESCRIPTOR));
            NewEntry->u.Cache.GroupMask.Mask = Entry->ProcessorMask;
        } else if (Entry->Relationship == YoriProcessorRelationProcessorPackage) {
            NewEntry->Relationship = YoriProcessorRelationProcessorPackage;
            NewEntry->SizeInBytes = sizeof(YORI_SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX);
            NewEntry->u.Processor.Flags = Entry->u.ProcessorCore.Flags;
            NewEntry->u.Processor.GroupCount = 1;
            NewEntry->u.Processor.GroupMask[0].Mask = Entry->ProcessorMask;
        }

        CurrentOffset += sizeof(YORI_SYSTEM_LOGICAL_PROCESSOR_INFORMATION);
        NewOffset += NewEntry->SizeInBytes;
        if (CurrentOffset >= BytesInBuffer) {
            break;
        }
        Entry = YoriLibAddToPointer(ProcInfo, CurrentOffset);
        NewEntry = YoriLibAddToPointer(CpuInfoContext->ProcInfo, NewOffset);
    }

    //
    //  Now populate group information.
    //

    NewEntry = CpuInfoContext->ProcInfo;
    NewEntry->u.Group.GroupInfo[0].ActiveProcessorMask = ProcessorsFound;

    ProcessorMask = 1;
    for (CurrentOffset = 0; CurrentOffset < (sizeof(DWORD_PTR) * 8); CurrentOffset++) {
        ProcessorMask = ProcessorMask<<1;
        if (ProcessorsFound & ProcessorMask) {
            NewEntry->u.Group.GroupInfo[0].MaximumProcessorCount++;
            NewEntry->u.Group.GroupInfo[0].ActiveProcessorCount++;
        }
    }

    YoriLibFree(ProcInfo);

    return TRUE;
}

#ifdef YORI_BUILTIN
/**
 The main entrypoint for the cpuinfo builtin command.
 */
#define ENTRYPOINT YoriCmd_YCPUINFO
#else
/**
 The main entrypoint for the cpuinfo standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the cpuinfo cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process, zero indicating success or nonzero on
         failure.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    BOOLEAN DisplayCores = FALSE;
    BOOLEAN DisplayGroups = FALSE;
    BOOLEAN DisplayNuma = FALSE;
    BOOLEAN DisplaySockets = FALSE;
    BOOLEAN DisplayFormatString = TRUE;
    BOOLEAN InsertNewline = FALSE;
    YORI_STRING Arg;
    CPUINFO_CONTEXT CpuInfoContext;
    YORI_STRING DisplayString;
    YORI_STRING AllocatedFormatString;
    LPTSTR DefaultFormatString = _T("Utilization: $UTILIZATION$\n")
                                 _T("Core count: $CORECOUNT$\n")
                                 _T("Group count: $GROUPCOUNT$\n")
                                 _T("Logical processors: $LOGICALCOUNT$\n")
                                 _T("Numa nodes: $NUMANODECOUNT$\n");
    LPTSTR DefaultUtilizationFormatString = _T("Utilization: $UTILIZATION$\n");

    ZeroMemory(&CpuInfoContext, sizeof(CpuInfoContext));
    CpuInfoContext.WaitTime = 300;
    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                CpuInfoHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2019-2021"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("a")) == 0) {
                DisplayCores = TRUE;
                DisplayGroups = TRUE;
                DisplayNuma = TRUE;
                DisplaySockets = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("c")) == 0) {
                DisplayCores = TRUE;
                DisplayFormatString = FALSE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("g")) == 0) {
                DisplayGroups = TRUE;
                DisplayFormatString = FALSE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("n")) == 0) {
                DisplayNuma = TRUE;
                DisplayFormatString = FALSE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("s")) == 0) {
                DisplaySockets = TRUE;
                DisplayFormatString = FALSE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("w")) == 0 &&
                       i + 1 < ArgC) {

                LONGLONG llTemp;
                DWORD CharsConsumed;

                llTemp = 0;
                if (YoriLibStringToNumber(&ArgV[i + 1], TRUE, &llTemp, &CharsConsumed) &&
                    CharsConsumed > 0) {

                    CpuInfoContext.WaitTime = (DWORD)llTemp;
                }
                i = i + 1;
                ArgumentUnderstood = TRUE;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    //
    //  If the Win7 API is not present, should fall back to the 2003 API and
    //  emulate the Win7 one.  If neither are present this app can't output
    //  topologicalinformation.
    //

    if (DllKernel32.pGetLogicalProcessorInformationEx != NULL) {
        if (!CpuInfoLoadProcessorInfo(&CpuInfoContext)) {
            return EXIT_FAILURE;
        }
        CpuInfoContext.TopologyLoaded = TRUE;
    } else if (DllKernel32.pGetLogicalProcessorInformation != NULL) {
        if (!CpuInfoLoadAndUpconvertProcessorInfo(&CpuInfoContext)) {
            return EXIT_FAILURE;
        }
        CpuInfoContext.TopologyLoaded = TRUE;
    }

    //
    //  Parse the processor information into summary counts.
    //

    if (CpuInfoContext.TopologyLoaded) {
        CpuInfoCountSummaries(&CpuInfoContext);
    } else if (DisplayCores || DisplayGroups || DisplayNuma || DisplaySockets) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("OS support not present\n"));
        return EXIT_FAILURE;
    }

    if (DisplayCores) {
        CpuInfoDisplayCores(&CpuInfoContext);
        InsertNewline = TRUE;
    }

    if (DisplayGroups) {
        if (InsertNewline) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("\n"));
        }
        CpuInfoDisplayGroups(&CpuInfoContext);
        InsertNewline = TRUE;
    }

    if (DisplayNuma) {
        if (InsertNewline) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("\n"));
        }
        CpuInfoDisplayNuma(&CpuInfoContext);
        InsertNewline = TRUE;
    }

    if (DisplaySockets) {
        if (InsertNewline) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("\n"));
        }
        CpuInfoDisplaySockets(&CpuInfoContext);
        InsertNewline = TRUE;
    }

    //
    //  Obtain a format string.
    //

    YoriLibInitEmptyString(&AllocatedFormatString);
    if (StartArg > 0) {
        if (!YoriLibBuildCmdlineFromArgcArgv(ArgC - StartArg, &ArgV[StartArg], TRUE, FALSE, &AllocatedFormatString)) {
            YoriLibFree(CpuInfoContext.ProcInfo);
            return EXIT_FAILURE;
        }
    } else {
        if (DisplayFormatString) {
            if (CpuInfoContext.TopologyLoaded) {
                YoriLibConstantString(&AllocatedFormatString, DefaultFormatString);
            } else {
                YoriLibConstantString(&AllocatedFormatString, DefaultUtilizationFormatString);
            }
        }
    }

    if (AllocatedFormatString.LengthInChars > 0) {

        if (InsertNewline) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("\n"));
        }

        //
        //  Output the format string with summary counts.
        //

        YoriLibInitEmptyString(&DisplayString);
        YoriLibExpandCommandVariables(&AllocatedFormatString, '$', FALSE, CpuInfoExpandVariables, &CpuInfoContext, &DisplayString);
        if (DisplayString.StartOfString != NULL) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%y"), &DisplayString);
            YoriLibFreeStringContents(&DisplayString);
        }
    }

    YoriLibFreeStringContents(&AllocatedFormatString);
    ASSERT(!CpuInfoContext.TopologyLoaded || CpuInfoContext.ProcInfo != NULL);
    if (CpuInfoContext.ProcInfo != NULL) {
        YoriLibFree(CpuInfoContext.ProcInfo);
    }

    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>Makefile<|end_filename|>

ANALYZE=0
ARCH=win32
DEBUG=0
FDI=0
PDB=1
YORI_BUILD_ID=0

!IF [$(CC) -? 2>&1 | findstr /C:"x64" >NUL 2>&1]==0
ARCH=amd64
!ELSE
!IF [$(CC) -? 2>&1 | findstr /C:"AMD64" >NUL 2>&1]==0
ARCH=amd64
!ELSE
!IF [$(CC) -? 2>&1 | findstr /C:"ARM64" >NUL 2>&1]==0
ARCH=arm64
!ELSE
!IF [$(CC) -? 2>&1 | findstr /C:"ARM" >NUL 2>&1]==0
ARCH=arm
!ELSE
!IF [$(CC) -? 2>&1 | findstr /C:"Itanium" >NUL 2>&1]==0
ARCH=ia64
!ELSE
!IF [$(CC) -? 2>&1 | findstr /C:"MIPS" >NUL 2>&1]==0
ARCH=mips
!ENDIF # MIPS
!ENDIF # Itanium
!ENDIF # ARM (32)
!ENDIF # ARM64
!ENDIF # AMD64
!ENDIF # x64

BINDIR_ROOT=bin\$(ARCH)
SYMDIR_ROOT=sym\$(ARCH)
MODDIR_ROOT=bin\$(ARCH)\modules

BINDIR=..\$(BINDIR_ROOT)
SYMDIR=..\$(SYMDIR_ROOT)
MODDIR=..\$(MODDIR_ROOT)

BUILD=$(MAKE) -nologo ANALYZE=$(ANALYZE) DEBUG=$(DEBUG) FDI=$(FDI) PDB=$(PDB) YORI_BUILD_ID=$(YORI_BUILD_ID) BINDIR=$(BINDIR) SYMDIR=$(SYMDIR) MODDIR=$(MODDIR)

CURRENTTIME=REM
!IFNDEF _YMAKE_VER
WRITECONFIGCACHEFILE=cache.mk
!ENDIF

all: all.real

!INCLUDE "config\common.mk"

!IF "$(FOR)"=="for"
STARTCMD=
!ELSE
STARTCMD="
!ENDIF

!IF [ydate.exe -? >NUL 2>&1]==0
CURRENTTIME=echo. & echo For: $(FOR) & ydate $$HOUR$$:$$MIN$$:$$SEC$$ & echo.
!ENDIF

SHDIRS=sh      \

DIRS=crt       \
     lib       \
     libwin    \
     libdlg    \
     libsh     \
     builtins  \
     assoc     \
     attrib    \
     cab       \
     cal       \
     charmap   \
     clip      \
     clmp      \
     cls       \
     co        \
     compact   \
     copy      \
     cpuinfo   \
     cshot     \
     cut       \
     cvtvt     \
     date      \
     df        \
     dir       \
     dircase   \
     du        \
     echo      \
     edit      \
     env       \
     envdiff   \
     erase     \
     err       \
     expr      \
     finfo     \
     for       \
     fscmp     \
     get       \
     grpcmp    \
     hash      \
     help      \
     hexdump   \
     hilite    \
     iconv     \
     initool   \
     intcmp    \
     kill      \
     lines     \
     lsof      \
     make      \
     mem       \
     mkdir     \
     mklink    \
     more      \
     mount     \
     move      \
     nice      \
     osver     \
     path      \
     pause     \
     petool    \
     pkg       \
     pkglib    \
     procinfo  \
     ps        \
     readline  \
     repl      \
     rmdir     \
     scut      \
     sdir      \
     setver    \
     shutdn    \
     sleep     \
     slmenu    \
     split     \
     sponge    \
     start     \
     strcmp    \
     stride    \
     sync      \
     tail      \
     tee       \
     timethis  \
     title     \
     touch     \
     type      \
     vhdtool   \
     vol       \
     which     \
     wininfo   \
     winpos    \
     ydbg      \
     ypm       \
     ysetup    \
     yui       \

!IFDEF _YMAKE_VER
$(BINDIR_ROOT):
	@-$(MKDIR) $(BINDIR_ROOT) 

$(BINDIR_ROOT)\YoriInit.d:
	@-$(MKDIR) $(BINDIR_ROOT)\YoriInit.d

$(SYMDIR_ROOT):
	@-$(MKDIR) $(SYMDIR_ROOT) 

$(MODDIR_ROOT):
	@-$(MKDIR) $(MODDIR_ROOT) 

all.real[dirs target=install]: $(DIRS) $(SHDIRS)

compile:

link[dirs target=link]: $(DIRS) $(SHDIRS)
!ELSE
all.real: writeconfigcache
	@$(CURRENTTIME)
	@$(FOR) %%i in ($(BINDIR_ROOT) $(SYMDIR_ROOT) $(MODDIR_ROOT) $(BINDIR_ROOT)\YoriInit.d) do $(STARTCMD)@if not exist %%i $(MKDIR) %%i$(STARTCMD)
	@$(FOR) %%i in ($(SHDIRS) $(DIRS)) do $(STARTCMD)@if exist %%i echo *** Compiling %%i & cd %%i & $(BUILD) compile READCONFIGCACHEFILE=..\$(WRITECONFIGCACHEFILE) & cd ..$(STARTCMD)
	@$(FOR) %%i in ($(DIRS)) do $(STARTCMD)@if exist %%i echo *** Linking %%i & cd %%i & $(BUILD) link READCONFIGCACHEFILE=..\$(WRITECONFIGCACHEFILE) & cd ..$(STARTCMD)
	@$(FOR) %%i in ($(SHDIRS)) do $(STARTCMD)@if exist %%i echo *** Linking %%i & cd %%i & $(BUILD) link READCONFIGCACHEFILE=..\$(WRITECONFIGCACHEFILE) & cd ..$(STARTCMD)
	@$(FOR) %%i in ($(SHDIRS) $(DIRS)) do $(STARTCMD)@if exist %%i echo *** Installing %%i & cd %%i & $(BUILD) install READCONFIGCACHEFILE=..\$(WRITECONFIGCACHEFILE) & cd ..$(STARTCMD)
	@$(CURRENTTIME)
!ENDIF

beta: all.real
	@if not exist beta $(MKDIR) beta
	@move $(BINDIR_ROOT) beta\$(ARCH)
	@move $(SYMDIR_ROOT) beta\$(ARCH)\sym

!IFDEF _YMAKE_VER
clean[dirs target=clean]: $(DIRS) $(SHDIRS)
!ELSE
clean: writeconfigcache
	@$(FOR) %%i in ($(SHDIRS) $(DIRS)) do $(STARTCMD)@if exist %%i echo *** Cleaning %%i & cd %%i & $(BUILD) clean READCONFIGCACHEFILE=..\$(WRITECONFIGCACHEFILE) & cd ..$(STARTCMD)
	@if exist *~ erase *~
	@$(FOR_ST) /D %%i in ($(MODDIR_ROOT) $(BINDIR_ROOT) $(SYMDIR_ROOT)) do @if exist %%i $(RMDIR) /s/q %%i
	@if exist $(WRITECONFIGCACHEFILE) erase $(WRITECONFIGCACHEFILE)
!ENDIF

distclean: clean
	@$(FOR_ST) /D %%i in (pkg\*) do @if exist %%i $(RMDIR) /s/q %%i
	@$(FOR_ST) %%i in (beta doc bin sym) do @if exist %%i $(RMDIR) /s/q %%i

help:
	@echo "ANALYZE=[0|1] - If set, will perform static analysis during compilation"
	@echo "DEBUG=[0|1] - If set, will compile debug build without optimization and with instrumentation"
	@echo "PDB=[0|1] - If set, will generate debug symbols"



<|start_filename|>sh/yoriproc.h<|end_filename|>
/**
 * @file sh/yoriproc.h
 *
 * Yori shell function declaration header file
 *
 * Copyright (c) 2017-2019 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

// *** ALIAS.C ***

__success(return)
BOOL
YoriShAddAlias(
    __in PYORI_STRING Alias,
    __in PYORI_STRING Value,
    __in BOOL Internal
    );

__success(return)
BOOL
YoriShAddAliasLiteral(
    __in LPTSTR Alias,
    __in LPTSTR Value,
    __in BOOL Internal
    );

__success(return)
BOOL
YoriShDeleteAlias(
    __in PYORI_STRING Alias
    );

__success(return)
BOOL
YoriShExpandAlias(
    __inout PYORI_LIBSH_CMD_CONTEXT CmdContext
    );

__success(return)
BOOL
YoriShExpandAliasFromString(
    __in PYORI_STRING CommandString,
    __out PYORI_STRING ExpandedString
    );

VOID
YoriShClearAllAliases(VOID);

/**
 Include user defined aliases in the output of YoriShGetAliasStrings .
 */
#define YORI_SH_GET_ALIAS_STRINGS_INCLUDE_USER     (0x00000001)

/**
 Include system defined aliases in the output of YoriShGetAliasStrings .
 */
#define YORI_SH_GET_ALIAS_STRINGS_INCLUDE_INTERNAL (0x00000002)

__success(return)
BOOL
YoriShGetAliasStrings(
    __in DWORD IncludeFlags,
    __inout PYORI_STRING AliasStrings
    );

__success(return)
BOOL
YoriShLoadSystemAliases(
    __in BOOL ImportFromCmd
    );

__success(return)
BOOL
YoriShMergeChangedAliasStrings(
    __in BOOL MergeFromCmd,
    __in PYORI_STRING OldStrings,
    __in PYORI_STRING NewStrings
    );

__success(return)
BOOL
YoriShGetSystemAliasStrings(
    __in BOOL LoadFromCmd,
    __out PYORI_STRING AliasBuffer
    );

// *** BUILTIN.C ***

extern CONST YORI_SH_BUILTIN_NAME_MAPPING YoriShBuiltins[];

DWORD
YoriShBuckPass (
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext,
    __in DWORD ExtraArgCount,
    ...
    );

DWORD
YoriShBuckPassToCmd (
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext
    );

__success(return)
BOOL
YoriShExecuteNamedModuleInProc(
    __in LPTSTR ModuleFileName,
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext,
    __out PDWORD ExitCode
    );

DWORD
YoriShBuiltIn (
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext
    );

__success(return)
BOOL
YoriShExecuteBuiltinString(
    __in PYORI_STRING Expression
    );

// *** COMPLETE.C ***

VOID
YoriShClearTabCompletionMatches(
    __inout PYORI_SH_INPUT_BUFFER Buffer
    );

/**
 If this flag is set, completion should match the full path rather than a
 relative file name.
 */
#define YORI_SH_TAB_COMPLETE_FULL_PATH      (0x00000001)

/**
 If this flag is set, completion should match command history rather than
 files or arguments.
 */
#define YORI_SH_TAB_COMPLETE_HISTORY        (0x00000002)

/**
 If this flag is set, completion should navigate backwards through the list
 of matches.
 */
#define YORI_SH_TAB_COMPLETE_BACKWARDS      (0x00000004)

/**
 If this flag is set, completion is for suggestions and should not populate
 with entries except containing a complete prefix match (ie., the suggestion
 is at the end.)
 */
#define YORI_SH_TAB_SUGGESTIONS             (0x00000008)

BOOLEAN
YoriShTabCompletion(
    __inout PYORI_SH_INPUT_BUFFER Buffer,
    __in DWORD TabFlags
    );

VOID
YoriShPrependParentDirectory(
    __inout PYORI_SH_INPUT_BUFFER Buffer
    );

VOID
YoriShTrimSuggestionList(
    __inout PYORI_SH_INPUT_BUFFER Buffer,
    __in PYORI_STRING NewString
    );

VOID
YoriShCompleteSuggestion(
    __inout PYORI_SH_INPUT_BUFFER Buffer
    );

// *** ENV.C ***

BOOL
YoriShIsEnvironmentVariableChar(
    __in TCHAR Char
    );

__success(return != 0)
DWORD
YoriShGetEnvironmentVariableWithoutSubstitution(
    __in LPCTSTR Name,
    __out_opt LPTSTR Variable,
    __in DWORD Size,
    __out_opt PDWORD Generation
    );

__success(return)
BOOL
YoriShGetEnvironmentVariable(
    __in LPCTSTR Name,
    __out_ecount_opt(Size) LPTSTR Variable,
    __in DWORD Size,
    __out PDWORD ReturnedSize,
    __out_opt PDWORD Generation
    );

__success(return)
BOOL
YoriShAllocateAndGetEnvironmentVariable(
    __in LPCTSTR Name,
    __out PYORI_STRING Value,
    __out_opt PDWORD Generation
    );

__success(return)
BOOL
YoriShGetEnvironmentVariableYS(
    __in PYORI_STRING VariableName,
    __out PYORI_STRING Value
    );

__success(return)
BOOL
YoriShExpandEnvironmentVariables(
    __in PYORI_STRING Expression,
    __out PYORI_STRING ResultingExpression,
    __inout_opt PDWORD CurrentOffset
    );

__success(return)
BOOL
YoriShSetEnvironmentVariable(
    __in PYORI_STRING VariableName,
    __in_opt PYORI_STRING Value
    );

__success(return)
BOOL
YoriShSetEnvironmentStrings(
    __in PYORI_STRING NewEnv
    );

// *** EXEC.C ***

DWORD
YoriShExecuteSingleProgram(
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext
    );

__success(return)
BOOL
YoriShExecuteExpressionAndCaptureOutput(
    __in PYORI_STRING Expression,
    __out PYORI_STRING ProcessOutput
    );

__success(return)
BOOL
YoriShExpandBackquotes(
    __in PYORI_STRING Expression,
    __out PYORI_STRING ResultingExpression
    );

__success(return)
BOOL
YoriShExecuteExpression(
    __in PYORI_STRING Expression
    );

// *** HISTORY.C ***

__success(return)
BOOL
YoriShAddToHistory(
    __in PYORI_STRING NewCmd,
    __in BOOLEAN IgnoreIfRepeat
    );

__success(return)
BOOL
YoriShAddToHistoryAndReallocate(
    __in PYORI_STRING NewCmd
    );

VOID
YoriShRemoveOneHistoryEntry(
    __in PYORI_SH_HISTORY_ENTRY HistoryEntry
    );

VOID
YoriShClearAllHistory(VOID);

__success(return)
BOOL
YoriShInitHistory(VOID);

__success(return)
BOOL
YoriShLoadHistoryFromFile(VOID);

__success(return)
BOOL
YoriShSaveHistoryToFile(VOID);

__success(return)
BOOL
YoriShGetHistoryStrings(
    __in DWORD MaximumNumber,
    __inout PYORI_STRING HistoryStrings
    );

// *** INPUT.C ***

__success(return)
BOOL
YoriShEnsureStringHasEnoughCharacters(
    __inout PYORI_STRING String,
    __in DWORD CharactersNeeded
    );

__success(return)
BOOLEAN
YoriShReplaceInputBufferTrackDirtyRange(
    __in PYORI_SH_INPUT_BUFFER Buffer,
    __in PYORI_STRING NewString
    );

__success(return)
BOOL
YoriShGetExpression(
    __out PYORI_STRING Expression
    );

VOID
YoriShCleanupInputContext(VOID);

// *** JOB.C ***

__success(return)
BOOL
YoriShCreateNewJob(
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext,
    __in HANDLE hProcess,
    __in DWORD dwProcessId
    );

__success(return)
BOOL
YoriShScanJobsReportCompletion(
    __in BOOL TeardownAll
    );

DWORD
YoriShGetNextJobId(
    __in DWORD PreviousJobId
    );

__success(return)
BOOL
YoriShJobSetPriority(
    __in DWORD JobId,
    __in DWORD PriorityClass
    );

__success(return)
BOOL
YoriShTerminateJob(
    __in DWORD JobId
    );

VOID
YoriShJobWait(
    __in DWORD JobId
    );

__success(return)
BOOL
YoriShGetJobOutput(
    __in DWORD JobId,
    __inout PYORI_STRING Output,
    __inout PYORI_STRING Errors
    );

__success(return)
BOOL
YoriShPipeJobOutput(
    __in DWORD JobId,
    __in_opt HANDLE hPipeOutput,
    __in_opt HANDLE hPipeErrors
    );

__success(return)
BOOL
YoriShGetJobInformation(
    __in DWORD JobId,
    __out PBOOL HasCompleted,
    __out PBOOL HasOutput,
    __out PDWORD ExitCode,
    __inout PYORI_STRING Command
    );

// *** MAIN.C ***

VOID
YoriShPreCommand(
    __in BOOLEAN EnableVt
    );

// *** PARSE.C ***

BOOLEAN
YoriShExpandEnvironmentInCmdContext(
    __inout PYORI_LIBSH_CMD_CONTEXT CmdContext
    );

__success(return)
BOOL
YoriShResolveCommandToExecutable(
    __in PYORI_LIBSH_CMD_CONTEXT CmdContext,
    __out PBOOL ExecutableFound
    );

// *** PROMPT.C ***
BOOL
YoriShDisplayPrompt(VOID);

BOOL
YoriShExecPreCommandString(VOID);

// *** RESTART.C ***

BOOL
YoriShSaveRestartState(VOID);

VOID
YoriShCleanupRestartSaveThreadIfCompleted(VOID);

BOOL
YoriShLoadSavedRestartState(
    __in PYORI_STRING ProcessId
    );

VOID
YoriShDiscardSavedRestartState(
    __in_opt PYORI_STRING ProcessId
    );

// *** WINDOW.C ***

/**
 Indicates the task has succeeded and the taskbar button should be green.
 */
#define YORI_SH_TASK_SUCCESS     0x01

/**
 Indicates the task has failed and the taskbar button should be red.
 */
#define YORI_SH_TASK_FAILED      0x02

/**
 Indicates the task is in progress and the taskbar button should be yellow.
 */
#define YORI_SH_TASK_IN_PROGRESS 0x03

/**
 Indicates the task is complete and the taskbar button should be normal.
 */
#define YORI_SH_TASK_COMPLETE    0x04

BOOL
YoriShSetWindowState(
    __in DWORD State
    );

BOOLEAN
YoriShCloseWindow(VOID);

// *** YORIFULL.C / YORISTD.C / YORINONE.C ***

BOOL
YoriShRegisterDefaultAliases(VOID);

// vim:sw=4:ts=4:et:


<|start_filename|>libsh/builtin.c<|end_filename|>
/**
 * @file /libsh/builtin.c
 *
 * Yori shell built in function registration
 *
 * Copyright (c) 2017 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>
#include <yorish.h>

/**
 Information related to builtins registered against this process.
 */
typedef struct _YORI_LIBSH_BUILTIN_GLOBAL {

    /**
     A list of currently loaded modules.
     */
    YORI_LIST_ENTRY LoadedModules;

    /**
     Hash table of builtin callbacks currently registered with Yori.
     */
    PYORI_HASH_TABLE Hash;

    /**
     A list of unload functions to invoke.  These are only for code statically
     linked into the shell executable, not loadable modules.  Once added, a
     callback is never removed - the code is guaranteed to still exist, so the
     worst case is calling something that has no work to do.
     */
    YORI_LIST_ENTRY UnloadCallbacks;

    /**
     Pointer to the active module, being the DLL that was most recently invoked
     by the shell.
     */
    PYORI_LIBSH_LOADED_MODULE ActiveModule;

    /**
     List of builtin callbacks currently registered with Yori.
     */
    YORI_LIST_ENTRY BuiltinCallbacks;

} YORI_LIBSH_BUILTIN_GLOBAL, *PYORI_LIBSH_BUILTIN_GLOBAL;

/**
 Information related to builtins registered against this process.
 */
YORI_LIBSH_BUILTIN_GLOBAL YoriLibShBuiltinGlobal;

/**
 A single callback function to invoke on shell exit that is part of the
 shell executable.
 */
typedef struct _YORI_LIBSH_BUILTIN_UNLOAD_CALLBACK {

    /**
     A list of unload notifications to make within the shell executable.
     This is paired with YoriLibShBuiltinUnloadCallbacks.
     */
    YORI_LIST_ENTRY ListEntry;

    /**
     Pointer to a function to call on shell exit.
     */
    PYORI_BUILTIN_UNLOAD_NOTIFY UnloadNotify;
} YORI_LIBSH_BUILTIN_UNLOAD_CALLBACK, *PYORI_LIBSH_BUILTIN_UNLOAD_CALLBACK;

/**
 Load a DLL file into a loaded module object that can be referenced.

 @param DllName Pointer to a NULL terminated string of a DLL to load.

 @return Pointer a referenced loaded module.  This module is linked into
         the loaded module list.  NULL on failure.
 */
PYORI_LIBSH_LOADED_MODULE
YoriLibShLoadDll(
    __in LPTSTR DllName
    )
{
    PYORI_LIBSH_LOADED_MODULE FoundEntry = NULL;
    PYORI_LIST_ENTRY ListEntry;
    DWORD DllNameLength;
    DWORD OldErrorMode;

    if (YoriLibShBuiltinGlobal.LoadedModules.Next != NULL) {
        ListEntry = YoriLibGetNextListEntry(&YoriLibShBuiltinGlobal.LoadedModules, NULL);
        while (ListEntry != NULL) {
            FoundEntry = CONTAINING_RECORD(ListEntry, YORI_LIBSH_LOADED_MODULE, ListEntry);
            if (YoriLibCompareStringWithLiteralInsensitive(&FoundEntry->DllName, DllName) == 0) {
                FoundEntry->ReferenceCount++;
                return FoundEntry;
            }

            ListEntry = YoriLibGetNextListEntry(&YoriLibShBuiltinGlobal.LoadedModules, ListEntry);
        }
    }

    DllNameLength = _tcslen(DllName);
    FoundEntry = YoriLibMalloc(sizeof(YORI_LIBSH_LOADED_MODULE) + (DllNameLength + 1) * sizeof(TCHAR));
    if (FoundEntry == NULL) {
        return NULL;
    }

    YoriLibInitEmptyString(&FoundEntry->DllName);
    FoundEntry->DllName.StartOfString = (LPTSTR)(FoundEntry + 1);
    FoundEntry->DllName.LengthInChars = DllNameLength;
    memcpy(FoundEntry->DllName.StartOfString, DllName, DllNameLength * sizeof(TCHAR));
    FoundEntry->ReferenceCount = 1;
    FoundEntry->UnloadNotify = NULL;

    //
    //  Disable the dialog if the file is not a valid DLL.  This might really
    //  be a DOS executable, not a DLL.
    //

    OldErrorMode = SetErrorMode(SEM_FAILCRITICALERRORS);

    FoundEntry->ModuleHandle = LoadLibrary(DllName);
    if (FoundEntry->ModuleHandle == NULL) {
        SetErrorMode(OldErrorMode);
        YoriLibFree(FoundEntry);
        return NULL;
    }
    SetErrorMode(OldErrorMode);

    if (YoriLibShBuiltinGlobal.LoadedModules.Next == NULL) {
        YoriLibInitializeListHead(&YoriLibShBuiltinGlobal.LoadedModules);
    }
    YoriLibAppendList(&YoriLibShBuiltinGlobal.LoadedModules, &FoundEntry->ListEntry);
    return FoundEntry;
}

/**
 Dereference a loaded DLL module and free it if the reference count reaches
 zero.

 @param LoadedModule The module to dereference.
 */
VOID
YoriLibShReleaseDll(
    __in PYORI_LIBSH_LOADED_MODULE LoadedModule
    )
{
    if (LoadedModule->ReferenceCount > 1) {
        LoadedModule->ReferenceCount--;
        return;
    }

    YoriLibRemoveListItem(&LoadedModule->ListEntry);
    if (LoadedModule->UnloadNotify != NULL) {
        LoadedModule->UnloadNotify();
    }
    if (LoadedModule->ModuleHandle != NULL) {
        FreeLibrary(LoadedModule->ModuleHandle);
    }
    YoriLibFree(LoadedModule);
}

/**
 Add a reference to a previously loaded DLL module.

 @param LoadedModule The module to reference.
 */
VOID
YoriLibShReferenceDll(
    __in PYORI_LIBSH_LOADED_MODULE LoadedModule
    )
{
    ASSERT(LoadedModule->ReferenceCount > 0);
    LoadedModule->ReferenceCount++;
}

/**
 Add a new function to invoke on shell exit or module unload.

 @param UnloadNotify Pointer to the function to invoke.

 @return TRUE if the callback was successfully added, FALSE if it was not.
 */
__success(return)
BOOL
YoriLibShSetUnloadRoutine(
    __in PYORI_BUILTIN_UNLOAD_NOTIFY UnloadNotify
    )
{
    if (YoriLibShBuiltinGlobal.ActiveModule != NULL) {

        if (YoriLibShBuiltinGlobal.ActiveModule->UnloadNotify == NULL) {
            YoriLibShBuiltinGlobal.ActiveModule->UnloadNotify = UnloadNotify;
        }

        if (YoriLibShBuiltinGlobal.ActiveModule->UnloadNotify == UnloadNotify) {
            return TRUE;
        }
    } else {

        PYORI_LIST_ENTRY ListEntry = NULL;
        PYORI_LIBSH_BUILTIN_UNLOAD_CALLBACK Callback;

        if (YoriLibShBuiltinGlobal.UnloadCallbacks.Next == NULL) {
            YoriLibInitializeListHead(&YoriLibShBuiltinGlobal.UnloadCallbacks);
        }

        ListEntry = YoriLibGetNextListEntry(&YoriLibShBuiltinGlobal.UnloadCallbacks, NULL);
        while (ListEntry != NULL) {
            Callback = CONTAINING_RECORD(ListEntry, YORI_LIBSH_BUILTIN_UNLOAD_CALLBACK, ListEntry);
            if (Callback->UnloadNotify == UnloadNotify) {
                return TRUE;
            }
            ListEntry = YoriLibGetNextListEntry(&YoriLibShBuiltinGlobal.UnloadCallbacks, ListEntry);
        }

        Callback = YoriLibMalloc(sizeof(YORI_LIBSH_BUILTIN_UNLOAD_CALLBACK));
        if (Callback == NULL) {
            return FALSE;
        }

        Callback->UnloadNotify = UnloadNotify;
        YoriLibAppendList(&YoriLibShBuiltinGlobal.UnloadCallbacks, &Callback->ListEntry);
        return TRUE;
    }

    return FALSE;
}

/**
 Associate a new builtin command with a function pointer to be invoked when
 the command is specified.

 @param BuiltinCmd The command to register.

 @param CallbackFn The function to invoke in response to the command.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOL
YoriLibShBuiltinRegister(
    __in PYORI_STRING BuiltinCmd,
    __in PYORI_CMD_BUILTIN CallbackFn
    )
{
    PYORI_LIBSH_BUILTIN_CALLBACK NewCallback;
    if (YoriLibShBuiltinGlobal.BuiltinCallbacks.Next == NULL) {
        YoriLibInitializeListHead(&YoriLibShBuiltinGlobal.BuiltinCallbacks);
    }
    if (YoriLibShBuiltinGlobal.Hash == NULL) {
        YoriLibShBuiltinGlobal.Hash = YoriLibAllocateHashTable(50);
        if (YoriLibShBuiltinGlobal.Hash == NULL) {
            return FALSE;
        }
    }

    NewCallback = YoriLibReferencedMalloc(sizeof(YORI_LIBSH_BUILTIN_CALLBACK) + (BuiltinCmd->LengthInChars + 1) * sizeof(TCHAR));
    if (NewCallback == NULL) {
        return FALSE;
    }

    YoriLibInitEmptyString(&NewCallback->BuiltinName);
    NewCallback->BuiltinName.StartOfString = (LPTSTR)(NewCallback + 1);
    NewCallback->BuiltinName.LengthInChars = BuiltinCmd->LengthInChars;
    memcpy(NewCallback->BuiltinName.StartOfString, BuiltinCmd->StartOfString, BuiltinCmd->LengthInChars * sizeof(TCHAR));
    NewCallback->BuiltinName.StartOfString[BuiltinCmd->LengthInChars] = '\0';
    YoriLibReference(NewCallback);
    NewCallback->BuiltinName.MemoryToFree = NewCallback;

    NewCallback->BuiltInFn = CallbackFn;
    if (YoriLibShBuiltinGlobal.ActiveModule != NULL) {
        YoriLibShBuiltinGlobal.ActiveModule->ReferenceCount++;
    }
    NewCallback->ReferencedModule = YoriLibShBuiltinGlobal.ActiveModule;

    //
    //  Insert at the front of the list so the most recently added entry
    //  will be found first, and the most recently added entry will be
    //  the first to be removed.
    //

    YoriLibInsertList(&YoriLibShBuiltinGlobal.BuiltinCallbacks, &NewCallback->ListEntry);
    YoriLibHashInsertByKey(YoriLibShBuiltinGlobal.Hash, &NewCallback->BuiltinName, NewCallback, &NewCallback->HashEntry);
    return TRUE;
}

/**
 Dissociate a previously associated builtin command such that the function is
 no longer invoked in response to the command.

 @param BuiltinCmd The command to unregister.

 @param CallbackFn The previously registered function to stop invoking in
        response to the command.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOL
YoriLibShBuiltinUnregister(
    __in PYORI_STRING BuiltinCmd,
    __in PYORI_CMD_BUILTIN CallbackFn
    )
{
    PYORI_LIBSH_BUILTIN_CALLBACK Callback;
    PYORI_HASH_ENTRY HashEntry;

    UNREFERENCED_PARAMETER(CallbackFn);

    if (YoriLibShBuiltinGlobal.Hash == NULL) {
        return FALSE;
    }

    if (YoriLibShBuiltinGlobal.Hash != NULL) {
        HashEntry = YoriLibHashRemoveByKey(YoriLibShBuiltinGlobal.Hash, BuiltinCmd);
        if (HashEntry != NULL) {
            Callback = (PYORI_LIBSH_BUILTIN_CALLBACK)HashEntry->Context;
            ASSERT(CallbackFn == Callback->BuiltInFn);
            YoriLibRemoveListItem(&Callback->ListEntry);
            if (Callback->ReferencedModule != NULL) {
                YoriLibShReleaseDll(Callback->ReferencedModule);
                Callback->ReferencedModule = NULL;
            }
            YoriLibFreeStringContents(&Callback->BuiltinName);
            YoriLibDereference(Callback);
        }
    }

    return FALSE;
}

/**
 Dissociate all previously associated builtin commands in preparation for
 process exit.
 */
VOID
YoriLibShBuiltinUnregisterAll(VOID)
{
    PYORI_LIST_ENTRY ListEntry;
    PYORI_LIBSH_BUILTIN_CALLBACK Callback;
    PYORI_LIBSH_BUILTIN_UNLOAD_CALLBACK UnloadCallback;

    if (YoriLibShBuiltinGlobal.BuiltinCallbacks.Next != NULL) {
        ListEntry = YoriLibGetNextListEntry(&YoriLibShBuiltinGlobal.BuiltinCallbacks, NULL);
        while (ListEntry != NULL) {
            Callback = CONTAINING_RECORD(ListEntry, YORI_LIBSH_BUILTIN_CALLBACK, ListEntry);
            YoriLibRemoveListItem(&Callback->ListEntry);
            YoriLibHashRemoveByEntry(&Callback->HashEntry);
            if (Callback->ReferencedModule != NULL) {
                YoriLibShReleaseDll(Callback->ReferencedModule);
                Callback->ReferencedModule = NULL;
            }
            YoriLibFreeStringContents(&Callback->BuiltinName);
            YoriLibDereference(Callback);
            ListEntry = YoriLibGetNextListEntry(&YoriLibShBuiltinGlobal.BuiltinCallbacks, NULL);
        }
    }

    if (YoriLibShBuiltinGlobal.Hash != NULL) {
        YoriLibFreeEmptyHashTable(YoriLibShBuiltinGlobal.Hash);
    }

    if (YoriLibShBuiltinGlobal.UnloadCallbacks.Next != NULL) {
        ListEntry = YoriLibGetNextListEntry(&YoriLibShBuiltinGlobal.UnloadCallbacks, NULL);
        while (ListEntry != NULL) {
            UnloadCallback = CONTAINING_RECORD(ListEntry, YORI_LIBSH_BUILTIN_UNLOAD_CALLBACK, ListEntry);
            ListEntry = YoriLibGetNextListEntry(&YoriLibShBuiltinGlobal.UnloadCallbacks, ListEntry);
            YoriLibRemoveListItem(&UnloadCallback->ListEntry);
            UnloadCallback->UnloadNotify();
            YoriLibFree(UnloadCallback);
        }
    }

    return;
}

/**
 Return the currently executing DLL module.  Code executing now is not part
 of the DLL, but it might be above this code in the stack.

 @return Pointer to the currently executing DLL module, or NULL if none is
         active.
 */
PYORI_LIBSH_LOADED_MODULE
YoriLibShGetActiveModule(VOID)
{
    return YoriLibShBuiltinGlobal.ActiveModule;
}

/**
 Set the currently executing DLL module.  Code executing now is not part of
 the DLL, but the DLL is about to be entered.  Alternatively, one DLL may
 have unwound and the previous DLL is being restored to be active.

 @param NewModule Pointer to the DLL module that is about to execute, or NULL
        if none is active.
 */
VOID
YoriLibShSetActiveModule(
    __in_opt PYORI_LIBSH_LOADED_MODULE NewModule
    )
{
    YoriLibShBuiltinGlobal.ActiveModule = NewModule;
}

/**
 Lookup a registered builtin function by case insensitive name.

 @param Name Pointer to the name of the function to look up.

 @return Pointer to the context describing this builtin function, or NULL if
         it is not found.
 */
PYORI_LIBSH_BUILTIN_CALLBACK
YoriLibShLookupBuiltinByName(
    __in PYORI_STRING Name
    )
{
    PYORI_HASH_ENTRY HashEntry;

    if (YoriLibShBuiltinGlobal.Hash == NULL) {
        return NULL;
    }

    HashEntry = YoriLibHashLookupByKey(YoriLibShBuiltinGlobal.Hash, Name);
    if (HashEntry == NULL) {
        return NULL;
    }

    return (PYORI_LIBSH_BUILTIN_CALLBACK)HashEntry->Context;
}

/**
 Return the previous (newer in time) builtin relative to a specified builtin.

 @param Existing Optionally points to a currently known builtin context.  If
        not specified, the oldest builtin is returned.

 @return Pointer to the previous callback context.
 */
PYORI_LIBSH_BUILTIN_CALLBACK
YoriLibShGetPreviousBuiltinCallback(
    __in_opt PYORI_LIBSH_BUILTIN_CALLBACK Existing
    )
{
    PYORI_LIST_ENTRY ListEntry;
    PYORI_LIBSH_BUILTIN_CALLBACK Previous;

    if (YoriLibShBuiltinGlobal.BuiltinCallbacks.Next == NULL) {
        return NULL;
    }

    if (Existing == NULL) {
        ListEntry = YoriLibGetPreviousListEntry(&YoriLibShBuiltinGlobal.BuiltinCallbacks, NULL);
    } else {
        ListEntry = YoriLibGetPreviousListEntry(&YoriLibShBuiltinGlobal.BuiltinCallbacks, &Existing->ListEntry);
    }

    if (ListEntry == NULL) {
        return NULL;
    }

    Previous = CONTAINING_RECORD(ListEntry, YORI_LIBSH_BUILTIN_CALLBACK, ListEntry);
    return Previous;
}


// vim:sw=4:ts=4:et:


<|start_filename|>path/path.c<|end_filename|>
/**
 * @file path/path.c
 *
 * Yori shell display file name components
 *
 * Copyright (c) 2017-2020 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 Help text to display to the user.
 */
const
CHAR strPathHelpText[] =
        "\n"
        "Converts relative paths into decomposable full paths.\n"
        "\n"
        "PATH [-license] [-e] [-f <fmtstring>] <path>\n"
        "\n"
        "   -e             Use an escaped long path\n"
        "\n"
        "Format specifiers are:\n"
        "   $BASE$         The file name without any path or extension\n"
        "   $DIR$          The directory hosting the file\n"
        "   $DRIVE$        The drive letter hosting the file\n"
        "   $EXT$          The file extension\n"
        "   $FILE$         The file name including extension\n"
        "   $PARENT$       The path to the parent of the file\n"
        "   $PATH$         The complete natural path to the file\n"
        "   $PATHNOSLASH$  The complete path to the file without trailing slashes\n"
        "   $SHARE$        The UNC share hosting the file\n";

/**
 Display usage text to the user.
 */
BOOL
PathHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Path %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strPathHelpText);
    return TRUE;
}

/**
 A decomposed form of a path.
 */
typedef struct _YORI_PATH_COMPONENTS {

    /**
     The entire path, including everything.
     */
    YORI_STRING EntireNaturalPath;

    /**
     The entire path, without trailing slashes.
     */
    YORI_STRING EntirePathWithoutTrailingSlash;

    /**
     A file extension, if present.  May contain NULL to indicate no extension
     was found, or could have a length of zero indicating a trailing period.
     */
    YORI_STRING Extension;

    /**
     The file name, without any extension.
     */
    YORI_STRING BaseName;

    /**
     The file name, including extension.
     */
    YORI_STRING FullFileName;

    /**
     The path from the root of the volume, excluding volume name.
     */
    YORI_STRING PathFromRoot;

    /**
     The drive letter of the volume.  Mutually exclusive with ShareName.
     */
    YORI_STRING DriveLetter;

    /**
     The share root of the volume.  Mutually exclusive with DriveLetter.
     */
    YORI_STRING ShareName;

    /**
     The path to the parent of the object.
     */
    YORI_STRING ParentName;
} YORI_PATH_COMPONENTS, *PYORI_PATH_COMPONENTS;

/**
 A callback function to expand any known variables found when parsing the
 path.

 @param OutputString A pointer to the output string to populate with data
        if a known variable is found.  The allocated length indicates the
        amount of the buffer that can be populated with data.

 @param VariableName The variable name to expand.

 @param Context Pointer to a YORI_PATH_COMPONENTS structure containing the
        data to populate.
 
 @return The number of characters successfully populated, or the number of
         characters required in order to successfully populate, or zero
         on error.
 */
DWORD
PathExpandVariables(
   __inout PYORI_STRING OutputString,
   __in PYORI_STRING VariableName,
   __in PVOID Context
   )
{
    DWORD CharsNeeded = 0;
    PYORI_PATH_COMPONENTS PathComponents = (PYORI_PATH_COMPONENTS)Context;

    if (YoriLibCompareStringWithLiteral(VariableName, _T("PATH")) == 0) {
        CharsNeeded = PathComponents->EntireNaturalPath.LengthInChars;
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("PATHNOSLASH")) == 0) {
        CharsNeeded = PathComponents->EntirePathWithoutTrailingSlash.LengthInChars;
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("EXT")) == 0) {
        CharsNeeded = PathComponents->Extension.LengthInChars;
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("FILE")) == 0) {
        CharsNeeded = PathComponents->FullFileName.LengthInChars;
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("BASE")) == 0) {
        CharsNeeded = PathComponents->BaseName.LengthInChars;
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("DRIVE")) == 0) {
        CharsNeeded = PathComponents->DriveLetter.LengthInChars;
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("DIR")) == 0) {
        CharsNeeded = PathComponents->PathFromRoot.LengthInChars;
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("SHARE")) == 0) {
        CharsNeeded = PathComponents->ShareName.LengthInChars;
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("PARENT")) == 0) {
        CharsNeeded = PathComponents->ParentName.LengthInChars;
    } else {
        return 0;
    }

    if (OutputString->LengthAllocated < CharsNeeded || CharsNeeded == 0) {
        return CharsNeeded;
    }

    if (YoriLibCompareStringWithLiteral(VariableName, _T("PATH")) == 0) {
        memcpy(OutputString->StartOfString, PathComponents->EntireNaturalPath.StartOfString, CharsNeeded * sizeof(TCHAR));
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("PATHNOSLASH")) == 0) {
        memcpy(OutputString->StartOfString, PathComponents->EntirePathWithoutTrailingSlash.StartOfString, CharsNeeded * sizeof(TCHAR));
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("EXT")) == 0) {
        memcpy(OutputString->StartOfString, PathComponents->Extension.StartOfString, CharsNeeded * sizeof(TCHAR));
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("FILE")) == 0) {
        memcpy(OutputString->StartOfString, PathComponents->FullFileName.StartOfString, CharsNeeded * sizeof(TCHAR));
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("BASE")) == 0) {
        memcpy(OutputString->StartOfString, PathComponents->BaseName.StartOfString, CharsNeeded * sizeof(TCHAR));
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("DRIVE")) == 0) {
        memcpy(OutputString->StartOfString, PathComponents->DriveLetter.StartOfString, CharsNeeded * sizeof(TCHAR));
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("DIR")) == 0) {
        memcpy(OutputString->StartOfString, PathComponents->PathFromRoot.StartOfString, CharsNeeded * sizeof(TCHAR));
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("SHARE")) == 0) {
        memcpy(OutputString->StartOfString, PathComponents->ShareName.StartOfString, CharsNeeded * sizeof(TCHAR));
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("PARENT")) == 0) {
        memcpy(OutputString->StartOfString, PathComponents->ParentName.StartOfString, CharsNeeded * sizeof(TCHAR));
    }

    OutputString->LengthInChars = CharsNeeded;
    return CharsNeeded;
}

#ifdef YORI_BUILTIN
/**
 The main entrypoint for the path builtin command.
 */
#define ENTRYPOINT YoriCmd_YPATH
#else
/**
 The main entrypoint for the path standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the path cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the child process on success, or failure if the child
         could not be launched.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    YORI_PATH_COMPONENTS PathComponents;
    LPTSTR FormatString = _T("$PATH$");
    YORI_STRING YsFormatString;
    YORI_STRING DisplayString;
    BOOL UseLongPath = FALSE;
    DWORD StartArg = 0;
    DWORD i;
    YORI_STRING Arg;

    YoriLibInitEmptyString(&YsFormatString);

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                PathHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2017-2020"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("e")) == 0) {
                UseLongPath = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("f")) == 0) {
                if (ArgC > i + 1) {
                    YsFormatString.StartOfString = ArgV[i + 1].StartOfString;
                    YsFormatString.LengthInChars = ArgV[i + 1].LengthInChars;
                    YsFormatString.LengthAllocated = ArgV[i + 1].LengthAllocated;
                    ArgumentUnderstood = TRUE;
                    i++;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                StartArg = i + 1;
                ArgumentUnderstood = TRUE;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    if (YsFormatString.StartOfString == NULL) {
        YoriLibConstantString(&YsFormatString, FormatString);
    }

    if (StartArg == 0 || StartArg == ArgC) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("path: missing argument\n"));
        return EXIT_FAILURE;
    }

    ZeroMemory(&PathComponents, sizeof(PathComponents));

    if (YoriLibUserStringToSingleFilePath(&ArgV[StartArg], UseLongPath, &PathComponents.EntireNaturalPath)) {
        DWORD CharIndex;
        BOOL ExtensionFound = FALSE;
        BOOL FileComponentFound = FALSE;
        DWORD KeepTrailingSlashesBefore;

        //
        //  Find the location where a natural path should retain trailing
        //  slashes.  This occurs because C: refers to a different file to
        //  C:\ , so C:\ would normally keep a trailing slash.
        //

        KeepTrailingSlashesBefore = 0;
        if (UseLongPath) {
            if (YoriLibIsPrefixedDriveLetterWithColonAndSlash(&PathComponents.EntireNaturalPath)) {
                KeepTrailingSlashesBefore = sizeof("\\\\?\\C:\\") - 1;
            }
        } else {
            if (YoriLibIsDriveLetterWithColonAndSlash(&PathComponents.EntireNaturalPath)) {
                KeepTrailingSlashesBefore = sizeof("C:\\") - 1;
            }
        }

        //
        //  Remove any trailing slashes up to the natural limit.
        //

        for (CharIndex = PathComponents.EntireNaturalPath.LengthInChars - 1; CharIndex > KeepTrailingSlashesBefore; CharIndex--) {
            if (PathComponents.EntireNaturalPath.StartOfString[CharIndex] == '\\') {
                PathComponents.EntireNaturalPath.LengthInChars--;
            } else {
                break;
            }

            if (CharIndex == 0) {
                break;
            }
        }

        //
        //  Remove any trailing slashes unconditionally.
        //

        PathComponents.EntirePathWithoutTrailingSlash.StartOfString = PathComponents.EntireNaturalPath.StartOfString;
        PathComponents.EntirePathWithoutTrailingSlash.LengthInChars = PathComponents.EntireNaturalPath.LengthInChars;

        for (CharIndex = PathComponents.EntirePathWithoutTrailingSlash.LengthInChars - 1; CharIndex > 0; CharIndex--) {
            if (PathComponents.EntirePathWithoutTrailingSlash.StartOfString[CharIndex] == '\\') {
                PathComponents.EntirePathWithoutTrailingSlash.LengthInChars--;
            } else {
                break;
            }

            if (CharIndex == 0) {
                break;
            }
        }

        //
        //  Count backwards to find the file name and extension
        //

        CharIndex = PathComponents.EntireNaturalPath.LengthInChars - 1;
        while(TRUE) {
            if (PathComponents.EntireNaturalPath.StartOfString[CharIndex] == '.' && !FileComponentFound && !ExtensionFound) {
                ExtensionFound = TRUE;
                PathComponents.Extension.StartOfString = &PathComponents.EntireNaturalPath.StartOfString[CharIndex + 1];
                PathComponents.Extension.LengthInChars = PathComponents.EntireNaturalPath.LengthInChars - CharIndex - 1;
            }

            if (PathComponents.EntireNaturalPath.StartOfString[CharIndex] == '\\' && !FileComponentFound) {
                FileComponentFound = TRUE;
                PathComponents.FullFileName.StartOfString = &PathComponents.EntireNaturalPath.StartOfString[CharIndex + 1];
                PathComponents.FullFileName.LengthInChars = PathComponents.EntireNaturalPath.LengthInChars - CharIndex - 1;

                PathComponents.BaseName.StartOfString = PathComponents.FullFileName.StartOfString;
                PathComponents.BaseName.LengthInChars = PathComponents.FullFileName.LengthInChars;
                if (PathComponents.Extension.StartOfString != NULL) {
                    PathComponents.BaseName.LengthInChars -= PathComponents.Extension.LengthInChars + 1;
                }

                PathComponents.ParentName.StartOfString = PathComponents.EntireNaturalPath.StartOfString;
                PathComponents.ParentName.LengthInChars = CharIndex;

                break;
            }

            if (CharIndex == 0) {
                break;
            }

            CharIndex--;
        }

        //
        //  Count forwards to find the drive letter or share
        //

        if (UseLongPath) {

            YORI_STRING PathAfterPrefix;

            //
            //  We kind of expect a long prefix if nothing else
            //

            if (PathComponents.EntireNaturalPath.LengthInChars < 4) {
                return EXIT_FAILURE;
            }

            YoriLibInitEmptyString(&PathAfterPrefix);
            PathAfterPrefix.StartOfString = PathComponents.EntireNaturalPath.StartOfString + 4;
            PathAfterPrefix.LengthInChars = PathComponents.EntireNaturalPath.LengthInChars - 4;

            if (YoriLibIsFullPathUnc(&PathComponents.EntireNaturalPath)) {
                BOOL EndOfServerNameFound = FALSE;

                //
                //  We have a \\?\UNC\ UNC prefix in an escaped path
                //

                for (CharIndex = 8; PathComponents.EntireNaturalPath.StartOfString[CharIndex] != '\0'; CharIndex++) {
                    if (PathComponents.EntireNaturalPath.StartOfString[CharIndex] == '\\') {
                        if (!EndOfServerNameFound) {
                            EndOfServerNameFound = TRUE;
                        } else {
                            break;
                        }
                    }
                }

                if (PathComponents.EntireNaturalPath.StartOfString[CharIndex] != 0 ||
                    EndOfServerNameFound) {

                    PathComponents.ShareName.StartOfString = PathComponents.EntireNaturalPath.StartOfString;
                    PathComponents.ShareName.LengthInChars = CharIndex;

                    //
                    //  If we have enough chars for a share name plus file name,
                    //  check for an intermediate directory.  If we don't, that
                    //  implies the file name is the last part of the share name,
                    //  so remove any reference to file name.
                    //

                    if (PathComponents.ShareName.LengthInChars + PathComponents.FullFileName.LengthInChars < PathComponents.EntireNaturalPath.LengthInChars) {

                        PathComponents.PathFromRoot.StartOfString = &PathComponents.EntireNaturalPath.StartOfString[CharIndex];
                        PathComponents.PathFromRoot.LengthInChars = PathComponents.EntireNaturalPath.LengthInChars - PathComponents.ShareName.LengthInChars - PathComponents.FullFileName.LengthInChars - 1;
                    } else if (PathComponents.ShareName.LengthInChars + PathComponents.FullFileName.LengthInChars > PathComponents.EntireNaturalPath.LengthInChars) {
                        PathComponents.BaseName.LengthInChars = 0;
                        PathComponents.FullFileName.LengthInChars = 0;
                        PathComponents.Extension.LengthInChars = 0;
                    }
                }
            } else if (YoriLibIsDriveLetterWithColonAndSlash(&PathAfterPrefix)) {

                //
                //  We have a drive letter, colon and slash in an escaped path
                //

                PathComponents.DriveLetter.StartOfString = &PathComponents.EntireNaturalPath.StartOfString[4];
                PathComponents.DriveLetter.LengthInChars = 1;

                PathComponents.PathFromRoot.StartOfString = &PathComponents.EntireNaturalPath.StartOfString[6];
                PathComponents.PathFromRoot.LengthInChars = PathComponents.EntireNaturalPath.LengthInChars - 6;

                if (PathComponents.FullFileName.StartOfString != NULL) {
                    PathComponents.PathFromRoot.LengthInChars -= PathComponents.FullFileName.LengthInChars + 1;
                }
            }
        } else {
            if (YoriLibIsDriveLetterWithColonAndSlash(&PathComponents.EntireNaturalPath)) {

                //
                //  We have a drive letter, colon and slash in a non escaped path
                //

                PathComponents.DriveLetter.StartOfString = &PathComponents.EntireNaturalPath.StartOfString[0];
                PathComponents.DriveLetter.LengthInChars = 1;

                PathComponents.PathFromRoot.StartOfString = &PathComponents.EntireNaturalPath.StartOfString[2];
                PathComponents.PathFromRoot.LengthInChars = PathComponents.EntireNaturalPath.LengthInChars - 2;

                if (PathComponents.FullFileName.StartOfString != NULL) {
                    PathComponents.PathFromRoot.LengthInChars -= PathComponents.FullFileName.LengthInChars + 1;
                }
            } else if ((PathComponents.EntireNaturalPath.StartOfString[0] == '\\') ||
                       (PathComponents.EntireNaturalPath.StartOfString[1] == '\\')) {

                BOOL EndOfServerNameFound = FALSE;

                //
                //  We have a \\ UNC prefix in a non escaped path
                //

                for (CharIndex = 2; PathComponents.EntireNaturalPath.StartOfString[CharIndex] != '\0'; CharIndex++) {
                    if (PathComponents.EntireNaturalPath.StartOfString[CharIndex] == '\\') {
                        if (!EndOfServerNameFound) {
                            EndOfServerNameFound = TRUE;
                        } else {
                            break;
                        }
                    }
                }

                if (PathComponents.EntireNaturalPath.StartOfString[CharIndex] != 0 ||
                    EndOfServerNameFound) {

                    PathComponents.ShareName.StartOfString = PathComponents.EntireNaturalPath.StartOfString;
                    PathComponents.ShareName.LengthInChars = CharIndex;

                    //
                    //  If we have enough chars for a share name plus file name,
                    //  check for an intermediate directory.  If we don't, that
                    //  implies the file name is the last part of the share name,
                    //  so remove any reference to file name.
                    //

                    if (PathComponents.ShareName.LengthInChars + PathComponents.FullFileName.LengthInChars < PathComponents.EntireNaturalPath.LengthInChars) {

                        PathComponents.PathFromRoot.StartOfString = &PathComponents.EntireNaturalPath.StartOfString[CharIndex];
                        PathComponents.PathFromRoot.LengthInChars = PathComponents.EntireNaturalPath.LengthInChars - PathComponents.ShareName.LengthInChars - PathComponents.FullFileName.LengthInChars - 1;
                    } else if (PathComponents.ShareName.LengthInChars + PathComponents.FullFileName.LengthInChars > PathComponents.EntireNaturalPath.LengthInChars) {
                        PathComponents.BaseName.LengthInChars = 0;
                        PathComponents.FullFileName.LengthInChars = 0;
                        PathComponents.Extension.LengthInChars = 0;
                    }
                }
            }
        }

        YoriLibInitEmptyString(&DisplayString);
        YoriLibExpandCommandVariables(&YsFormatString, '$', FALSE, PathExpandVariables, &PathComponents, &DisplayString);
        YoriLibFreeStringContents(&PathComponents.EntireNaturalPath);
        if (DisplayString.StartOfString != NULL) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%y\n"), &DisplayString);
            YoriLibFreeStringContents(&DisplayString);
        }
    }

    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>libsh/yorish.h<|end_filename|>
/**
 * @file libsh/yorish.h
 *
 * Header for library routines that are of value to the shell or other
 * components performing very shell like behavior.
 *
 * Copyright (c) 2017-2020 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */


/**
 Information about each argument in an enumerated list of arguments.
 */
typedef struct _YORI_LIBSH_ARG_CONTEXT {

    /**
     TRUE if the argument is enclosed in quotes.
     */
    BOOLEAN Quoted;

    /**
     TRUE if the closing quote is present on the argument.  In rare cases it
     is possible to have a terminating quote of a non-quoted argument.
     This happens in cases such as '>"foo bar"'.  This logic still treats
     that argument as having a terminating quote to suppress suggestions
     after the quote; in general this value should only be used in
     conjunction with Quoted (indicating a leading quote) above.
     */
    BOOLEAN QuoteTerminated;

} YORI_LIBSH_ARG_CONTEXT, *PYORI_LIBSH_ARG_CONTEXT;

/**
 A command line that has been broken up into a series of arguments.
 */
typedef struct _YORI_LIBSH_CMD_CONTEXT {

    /**
     The number of arguments.
     */
    DWORD ArgC;

    /**
     An array of pointers to each argument.  Each of these arguments has been
     referenced and should be dereferenced when no longer needed.
     */
    PYORI_STRING ArgV;

    /**
     An array of information about each argument, including the object that
     was referenced for each.
     */
    PYORI_LIBSH_ARG_CONTEXT ArgContexts;

    /**
     When generating the command context, if a string offset is specified,
     this value contains the argument that the string offset would correspond
     to.
     */
    DWORD CurrentArg;

    /**
     When generating the command context, if a string offset is specified,
     this value contains the offset within the current argument that it would
     correspond to.
     */
    DWORD CurrentArgOffset;

    /**
     Memory to dereference when the context is torn down.  Typically this
     single allocation backs the argv and ArgContexts array, and often backs
     the contents of each of the arguments also.
     */
    PVOID MemoryToFree;

    /**
     TRUE if there are characters in the string following the final argument.
     FALSE if the final character in the string is part of an argument.
     */
    BOOL TrailingChars;
} YORI_LIBSH_CMD_CONTEXT, *PYORI_LIBSH_CMD_CONTEXT;

/**
 For every process that the shell launches which is subject to debugging, it
 may receive debug events from child processes launched by that process.
 These are otherwise unknown to the shell.  For the most part, it has no
 reason to care about these processes, except the minimum to allow the
 debugging logic to operate correctly.
 */
typedef struct _YORI_LIBSH_DEBUGGED_CHILD_PROCESS {

    /**
     The list of this debugged child within the process that was launched
     directly from the shell.
     */
    YORI_LIST_ENTRY ListEntry;

    /**
     A handle to the process.  This has been duplicated within this program
     and must be closed when freeing this structure.
     */
    HANDLE hProcess;

    /**
     A handle to the initial thread within the process.  This has been
     duplicated within this program and must be closed when freeing this
     structure.
     */
    HANDLE hInitialThread;

    /**
     The process identifier for this process.
     */
    DWORD dwProcessId;

    /**
     The identifier for the initial thread within the process.
     */
    DWORD dwInitialThreadId;

} YORI_LIBSH_DEBUGGED_CHILD_PROCESS, *PYORI_LIBSH_DEBUGGED_CHILD_PROCESS;

/**
 Information about how to execute a single program.  The program may be
 internal or external.
 */
typedef struct _YORI_LIBSH_SINGLE_EXEC_CONTEXT {

    /**
     The set of arguments to invoke the program with.
     */
    YORI_LIBSH_CMD_CONTEXT CmdToExec;

    /**
     Pointer to the next program in an execution chain or NULL if there is
     no next program.
     */
    struct _YORI_LIBSH_SINGLE_EXEC_CONTEXT * NextProgram;

    /**
     Reference count.  This structure should not be cleaned up or deallocated
     until this reaches zero.  Synchronized via Interlock.  Note that while
     an exec context is part of an exec plan, a reference is held from the
     plan.
     */
    DWORD ReferenceCount;

    /**
     Specifies the type of the next program and the conditions under which
     it should execute.
     */
    enum {
        NextProgramTypeNone = 0,
        NextProgramExecUnconditionally = 1,
        NextProgramExecConcurrently = 2,
        NextProgramExecOnFailure = 3,
        NextProgramExecOnSuccess = 4,
        NextProgramExecNever = 5
    } NextProgramType;

    /**
     Specifies the origin of stdin when invoking the program.
     */
    enum {
        StdInTypeDefault = 1,
        StdInTypeFile = 2,
        StdInTypeNull = 3,
        StdInTypePipe = 4
    } StdInType;

    /**
     Extra information specific to each type of stdin origin.
     */
    union {
        struct {
            YORI_STRING FileName;
        } File;
        struct {
            HANDLE PipeFromPriorProcess;
        } Pipe;
    } StdIn;

    /**
     Specifies the target of stdout when invoking the program.
     */
    enum {
        StdOutTypeDefault = 1,
        StdOutTypeOverwrite = 2,
        StdOutTypeAppend = 3,
        StdOutTypeNull = 4,
        StdOutTypePipe = 5,
        StdOutTypeBuffer = 6,
        StdOutTypeStdErr = 7
    } StdOutType;

    /**
     Extra information specific to each type of stdout target.
     */
    union {
        struct {
            YORI_STRING FileName;
        } Overwrite;
        struct {
            YORI_STRING FileName;
        } Append;
        struct {
            HANDLE PipeFromProcess;
            PVOID ProcessBuffers;
            BOOLEAN RetainBufferData;
        } Buffer;
    } StdOut;

    /**
     Specifies the target of stderr when invoking the program.
     */
    enum {
        StdErrTypeDefault = 1,
        StdErrTypeOverwrite = 2,
        StdErrTypeAppend = 3,
        StdErrTypeStdOut = 4,
        StdErrTypeNull = 5,
        StdErrTypeBuffer = 6
    } StdErrType;

    /**
     Extra information specific to each type of stderr target.
     */
    union {
        struct {
            YORI_STRING FileName;
        } Overwrite;
        struct {
            YORI_STRING FileName;
        } Append;
        struct {
            HANDLE PipeFromProcess;
            PVOID ProcessBuffers;
            BOOLEAN RetainBufferData;
        } Buffer;
    } StdErr;

    /**
     If the process has been launched, contains a handle to the child
     process.
     */
    HANDLE hProcess;

    /**
     Handle to the primary thread of the child process.
     */
    HANDLE hPrimaryThread;

    /**
     If the child process is being debugged, a list of ancestor processes
     which the debugging engine needs to reason about.
     */
    YORI_LIST_ENTRY DebuggedChildren;

    /**
     Handle to a debugger thread, if the child process is being debugged.
     */
    HANDLE hDebuggerThread;

    /**
     The process identifier of the child process if it has been launched.
     For some reason some APIs want this and others want the handle.
     */
    DWORD dwProcessId;

    /**
     TRUE if when the program is executed we should wait for it to complete.
     If FALSE, execution can resume immediately, either executing the next
     program or returning to the user for more input.
     */
    BOOLEAN WaitForCompletion;

    /**
     TRUE if any escape characters should be used literally (not removed)
     in any child process string.  This is used when the child process is
     being generated implicitly, for example an implied subshell.  If the
     user consciously generated a command, the shell would normally remove
     any escapes before the child command gets the string.
     */
    BOOLEAN IncludeEscapesAsLiteral;

    /**
     TRUE if the program should be executed on a different console to the
     one the user is operating on.
     */
    BOOLEAN RunOnSecondConsole;

    /**
     TRUE if the environment should be reloaded from the process on
     termination.  This implies WaitForCompletion is TRUE.
     */
    BOOLEAN CaptureEnvironmentOnExit;

    /**
     TRUE if the process was being waited upon and the user switched
     windows, triggering the shell to indicate completion in the
     taskbar.
     */
    BOOLEAN TaskCompletionDisplayed;

    /**
     Set to TRUE to indicate the process should not attempt to display task
     completion because it is believed to not be an active background
     process.
     */
    BOOLEAN SuppressTaskCompletion;

    /**
     Set to TRUE to indicate that the debug pump thread has completed
     processing.  This is currently only used for debugging, because the
     debug pump thread is still running at the point of the final dereference,
     even though it has completed processing debug messages.
     */
    BOOLEAN DebugPumpThreadFinished;

    /**
     Set to TRUE to indicate that the process should be given a chance to
     terminate gracefully with GenerateConsoleCtrlEvent.  If FALSE, the only
     way to end the process is with TerminateProcess.
     */
    BOOLEAN TerminateGracefully;

} YORI_LIBSH_SINGLE_EXEC_CONTEXT, *PYORI_LIBSH_SINGLE_EXEC_CONTEXT;

/**
 When programs are executed, they temporarily modify the stdin/stdout/stderr
 of the shell process.  This structure contains information needed to revert
 back to the previous behavior.
 */
typedef struct _YORI_LIBSH_PREVIOUS_REDIRECT_CONTEXT {

    /**
     TRUE if stdin needs to be reset.
     */
    BOOLEAN ResetInput;

    /**
     TRUE if stdout needs to be reset.
     */
    BOOLEAN ResetOutput;

    /**
     TRUE if stderr needs to be reset.
     */
    BOOLEAN ResetError;

    /**
     TRUE if stdout and stderr have been modified to refer to the same
     location.
     */
    BOOLEAN StdErrRedirectsToStdOut;

    /**
     TRUE if stdout and stderr have been modified to refer to the same
     location.
     */
    BOOLEAN StdOutRedirectsToStdErr;

    /**
     A handle to the original stdin.
     */
    HANDLE StdInput;

    /**
     A handle to the original stdout.
     */
    HANDLE StdOutput;

    /**
     A handle to the original stderr.
     */
    HANDLE StdError;
} YORI_LIBSH_PREVIOUS_REDIRECT_CONTEXT, *PYORI_LIBSH_PREVIOUS_REDIRECT_CONTEXT;

/**
 A plan to execute multiple programs.
 */
typedef struct _YORI_LIBSH_EXEC_PLAN {

    /**
     Pointer to the first program to execute.  It will link to subsequent
     programs to execute.  This is used when each program is executed by this
     instance of the shell; if the shell decides to create a subshell, the
     EntireCmd below is used instead.
     */
    PYORI_LIBSH_SINGLE_EXEC_CONTEXT FirstCmd;

    /**
     The total number of programs in the FirstCmd program list.
     */
    ULONG NumberCommands;

    /**
     The entire command to execute.  This is used if execution decides to hand
     this plan to a subshell process.  If the current shell is executing the
     plan, the FirstCmd list above is used instead.
     */
    YORI_LIBSH_SINGLE_EXEC_CONTEXT EntireCmd;

    /**
     TRUE if the process was being waited upon and the user switched
     windows, triggering the shell to indicate completion in the
     taskbar.
     */
    BOOLEAN TaskCompletionDisplayed;

    /**
     TRUE if when the plan is executed we should wait for it to complete.
     If FALSE, input can resume immediately.  This may require sending the
     plan to a child process which can execute the plan while the parent
     process continues to prompt for input.
     */
    BOOLEAN WaitForCompletion;

} YORI_LIBSH_EXEC_PLAN, *PYORI_LIBSH_EXEC_PLAN;

/**
 A structure containing information about a currently loaded DLL.
 */
typedef struct _YORI_LIBSH_LOADED_MODULE {

    /**
     The entry for this loaded module on the list of actively loaded
     modules.
     */
    YORI_LIST_ENTRY ListEntry;

    /**
     A string describing the DLL file name.
     */
    YORI_STRING DllName;

    /**
     A callback function to invoke on unload to facilitate cleanup.
     */
    PYORI_BUILTIN_UNLOAD_NOTIFY UnloadNotify;

    /**
     The reference count of this module.
     */
    ULONG ReferenceCount;

    /**
     A handle to the DLL.
     */
    HANDLE ModuleHandle;
} YORI_LIBSH_LOADED_MODULE, *PYORI_LIBSH_LOADED_MODULE;

/**
 A structure containing an individual builtin callback.
 */
typedef struct _YORI_LIBSH_BUILTIN_CALLBACK {

    /**
     Links between the registered builtin callbacks.
     */
    YORI_LIST_ENTRY ListEntry;

    /**
     The name of the callback.
     */
    YORI_STRING BuiltinName;

    /**
     The hash entry for this match.
     */
    YORI_HASH_ENTRY HashEntry;

    /**
     A function pointer to the builtin.
     */
    PYORI_CMD_BUILTIN BuiltInFn;

    /**
     Pointer to a referenced module that implements this builtin function.
     This may be NULL if it's a function statically linked into the main
     executable.
     */
    PYORI_LIBSH_LOADED_MODULE ReferencedModule;

} YORI_LIBSH_BUILTIN_CALLBACK, *PYORI_LIBSH_BUILTIN_CALLBACK;

// *** BUILTIN.C ***

PYORI_LIBSH_LOADED_MODULE
YoriLibShLoadDll(
    __in LPTSTR DllName
    );

VOID
YoriLibShReleaseDll(
    __in PYORI_LIBSH_LOADED_MODULE LoadedModule
    );

VOID
YoriLibShReferenceDll(
    __in PYORI_LIBSH_LOADED_MODULE LoadedModule
    );

PYORI_LIBSH_LOADED_MODULE
YoriLibShGetActiveModule(VOID);

VOID
YoriLibShSetActiveModule(
    __in_opt PYORI_LIBSH_LOADED_MODULE NewModule
    );

PYORI_LIBSH_BUILTIN_CALLBACK
YoriLibShLookupBuiltinByName(
    __in PYORI_STRING Name
    );

PYORI_LIBSH_BUILTIN_CALLBACK
YoriLibShGetPreviousBuiltinCallback(
    __in_opt PYORI_LIBSH_BUILTIN_CALLBACK Existing
    );

__success(return)
BOOL
YoriLibShBuiltinRegister(
    __in PYORI_STRING BuiltinCmd,
    __in PYORI_CMD_BUILTIN CallbackFn
    );

__success(return)
BOOL
YoriLibShBuiltinUnregister(
    __in PYORI_STRING BuiltinCmd,
    __in PYORI_CMD_BUILTIN CallbackFn
    );

VOID
YoriLibShBuiltinUnregisterAll(VOID);

__success(return)
BOOL
YoriLibShSetUnloadRoutine(
    __in PYORI_BUILTIN_UNLOAD_NOTIFY UnloadNotify
    );

// *** CMDBUF.C ***

__success(return)
BOOL
YoriLibShCreateNewProcessBuffer(
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext
    );

__success(return)
BOOL
YoriLibShAppendToExistingProcessBuffer(
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext
    );

__success(return)
BOOL
YoriLibShForwardProcessBufferToNextProcess(
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext
    );

VOID
YoriLibShDereferenceProcessBuffer(
    __in PVOID ThisBuffer
    );

VOID
YoriLibShReferenceProcessBuffer(
    __in PVOID ThisBuffer
    );

__success(return)
BOOL
YoriLibShGetProcessOutputBuffer(
    __in PVOID ThisBuffer,
    __out PYORI_STRING String
    );

__success(return)
BOOL
YoriLibShGetProcessErrorBuffer(
    __in PVOID ThisBuffer,
    __out PYORI_STRING String
    );

VOID
YoriLibShTeardownProcessBuffersIfCompleted(
    __in PVOID ThisBuffer
    );

__success(return)
BOOL
YoriLibShScanProcessBuffersForTeardown(
    __in BOOL TeardownAll
    );

__success(return)
BOOL
YoriLibShWaitForProcessBufferToFinalize(
    __in PVOID ThisBuffer
    );

__success(return)
BOOL
YoriLibShPipeProcessBuffers(
    __in PVOID ThisBuffer,
    __in_opt HANDLE hPipeOutput,
    __in_opt HANDLE hPipeErrors
    );

// *** EXEC.C ***

VOID
YoriLibShCleanupFailedProcessLaunch(
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext
    );

VOID
YoriLibShCommenceProcessBuffersIfNeeded(
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext
    );

DWORD
YoriLibShCreateProcess(
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext,
    __in_opt LPTSTR CurrentDirectory,
    __out_opt PBOOL FailedInRedirection
    );

DWORD
YoriLibShInitializeRedirection(
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext,
    __in BOOL PrepareForBuiltIn,
    __out PYORI_LIBSH_PREVIOUS_REDIRECT_CONTEXT PreviousRedirectContext
    );

VOID
YoriLibShRevertRedirection(
    __in PYORI_LIBSH_PREVIOUS_REDIRECT_CONTEXT PreviousRedirectContext
    );

BOOLEAN
YoriLibShBuildCmdContextForCmdBuckPass (
    __out PYORI_LIBSH_CMD_CONTEXT CmdContext,
    __in PYORI_STRING CmdLine
    );

// *** PARSE.C ***

__success(return)
BOOLEAN
YoriLibShAllocateArgCount(
    __out PYORI_LIBSH_CMD_CONTEXT CmdContext,
    __in DWORD ArgCount,
    __in DWORD ExtraByteCount,
    __out_opt PVOID *ExtraData
    );

__success(return)
BOOLEAN
YoriLibShBuildCmdlineFromCmdContext(
    __in PYORI_LIBSH_CMD_CONTEXT CmdContext,
    __inout PYORI_STRING CmdLine,
    __in BOOL RemoveEscapes,
    __out_opt PDWORD BeginCurrentArg,
    __out_opt PDWORD EndCurrentArg
    );

VOID
YoriLibShCheckIfArgNeedsQuotes(
    __in PYORI_LIBSH_CMD_CONTEXT CmdContext,
    __in DWORD ArgIndex
    );

__success(return)
BOOL
YoriLibShCopyCmdContext(
    __out PYORI_LIBSH_CMD_CONTEXT DestCmdContext,
    __in PYORI_LIBSH_CMD_CONTEXT SrcCmdContext
    );

VOID
YoriLibShCopyArg(
    __in PYORI_LIBSH_CMD_CONTEXT SrcCmdContext,
    __in DWORD SrcArgument,
    __in PYORI_LIBSH_CMD_CONTEXT DestCmdContext,
    __in DWORD DestArgument
    );

VOID
YoriLibShDereferenceExecContext(
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext,
    __in BOOLEAN Deallocate
    );

__success(return)
BOOL
YoriLibShFindBestBackquoteSubstringAtOffset(
    __in PYORI_STRING String,
    __in DWORD StringOffset,
    __out PYORI_STRING CurrentSubset
    );

__success(return)
BOOL
YoriLibShFindNextBackquoteSubstring(
    __in PYORI_STRING String,
    __out PYORI_STRING CurrentSubset,
    __out PDWORD CharsInPrefix
    );

VOID
YoriLibShFreeCmdContext(
    __in PYORI_LIBSH_CMD_CONTEXT CmdContext
    );

VOID
YoriLibShFreeExecPlan(
    __in PYORI_LIBSH_EXEC_PLAN ExecPlan
    );

__success(return)
BOOLEAN
YoriLibShIsArgumentSeperator(
    __in PYORI_STRING String,
    __out_opt PDWORD CharsToConsumeOut,
    __out_opt PBOOLEAN TerminateArgOut
    );

__success(return)
BOOL
YoriLibShParseCmdContextToExecPlan(
    __in PYORI_LIBSH_CMD_CONTEXT CmdContext,
    __out PYORI_LIBSH_EXEC_PLAN ExecPlan,
    __out_opt PYORI_LIBSH_SINGLE_EXEC_CONTEXT* CurrentExecContext,
    __out_opt PBOOL CurrentArgIsForProgram,
    __out_opt PDWORD CurrentArgIndex,
    __out_opt PDWORD CurrentArgOffset
    );

__success(return)
BOOLEAN
YoriLibShParseCmdlineToCmdContext(
    __in PYORI_STRING CmdLine,
    __in DWORD CurrentOffset,
    __out PYORI_LIBSH_CMD_CONTEXT CmdContext
    );

VOID
YoriLibShReferenceExecContext(
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext
    );

__success(return)
BOOLEAN
YoriLibShRemoveEscapesFromArgCArgV(
    __in DWORD ArgC,
    __inout PYORI_STRING ArgV
    );

__success(return)
BOOLEAN
YoriLibShRemoveEscapesFromCmdContext(
    __in PYORI_LIBSH_CMD_CONTEXT EscapedCmdContext,
    __out PYORI_LIBSH_CMD_CONTEXT NoEscapedCmdContext
    );

// vim:sw=4:ts=4:et:


<|start_filename|>edit/Makefile<|end_filename|>

BINARIES=yedit.exe

!INCLUDE "..\config\common.mk"

!IF $(PDB)==1
LINKPDB=/Pdb:yedit.pdb
!ENDIF

BIN_OBJS=\
	 edit.obj         \
	 about.obj        \
	 options.obj      \

MOD_OBJS=\
	 mod_edit.obj     \
	 about.obj        \
	 options.obj      \

compile: $(BIN_OBJS) builtins.lib

yedit.exe: $(BIN_OBJS) $(YORILIB) $(YORICRT) $(YORIWIN) $(YORIDLG) $(YORIVER)
	@echo $@
	@$(LINK) $(LDFLAGS) -entry:$(YENTRY) $(BIN_OBJS) $(LIBS) $(CRTLIB) $(YORILIB) $(YORIWIN) $(YORIDLG) $(YORIVER) -version:$(YORI_VER_MAJOR).$(YORI_VER_MINOR) $(LINKPDB) -out:$@

mod_edit.obj: edit.c
	@echo $@
	@$(CC) -c -DYORI_BUILTIN=1 $(CFLAGS) -Fo$@ edit.c

builtins.lib: $(MOD_OBJS)
	@echo $@
	@$(LIB32) $(LIBFLAGS) $(MOD_OBJS) -out:$@


<|start_filename|>sdir/sdir.c<|end_filename|>
/**
 * @file sdir/sdir.c
 *
 * Colorful, sorted and optionally rich directory enumeration
 * for Windows.
 *
 * This module implements the core logic of displaying directories.
 *
 * Copyright (c) 2014-2018 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "sdir.h"


/**
 Specifies the number of directory entries that are currently allocated.
 */
DWORD SdirAllocatedDirents;

/**
 Pointer to an array of directory entries.  This corresponds to files in
 a single directory, populated in response to enumerate.
 */
PYORI_FILE_INFO SdirDirCollection;

/**
 Pointer to an array of pointers to directory entries.  These pointers
 are maintained sorted based on the user's sort criteria so that files
 can be displayed in order from this indirection.
 */
PYORI_FILE_INFO * SdirDirSorted;

/**
 Specifies the number of allocated directory entries that have been
 populated with files returned from directory enumerate.
 */
ULONG SdirDirCollectionCurrent;

/**
 Specifies the longest file name found so far when enumerating through a
 single directory.  This length in in characters.
 */
ULONG SdirDirCollectionLongest;

/**
 Specifies the total length of characters stored in file names during
 enumeration of a single directory.  This is used to truncate file names
 if they start to look too long.
 */
ULONG SdirDirCollectionTotalNameLength;

/**
 Pointer to a dynamically allocated options structure which contains run
 time configuration about the application.
 */
PSDIR_OPTS Opts;

/**
 Pointer to a dynamically allocated summary structure recording the number
 of files and directories found during enumerate and their sizes.
 */
PSDIR_SUMMARY Summary;

/**
 Global data for the application.
 */
SDIR_GLOBAL SdirGlobal;

BOOL
SdirDisplayCollection(VOID);

/**
 Capture all required information from a file found by the system into a
 directory entry.

 @param CurrentEntry Pointer to a directory entry to populate with
        information.

 @param FindData Information returned by the system when enumerating files.

 @param FullPath Pointer to a string referring to the full path to the file.

 @param ForceDisplay If TRUE, suppress processing to hide the entry because it
        needs to be displayed unconditionally.  This is used for directory
        headers etc.  If FALSE, the regular user specified rules are applied.
 */
VOID
SdirCaptureFoundItemIntoDirent (
    __out PYORI_FILE_INFO CurrentEntry,
    __in PWIN32_FIND_DATA FindData,
    __in PYORI_STRING FullPath,
    __in BOOL ForceDisplay
    ) 
{
    DWORD i;

    memset(CurrentEntry, 0, sizeof(*CurrentEntry));

    //
    //  Copy over the data from Win32's FindFirstFile into our own structure.
    //

    for (i = 0; i < SdirGetNumSdirOptions(); i++) {

        PSDIR_FEATURE Feature;
        Feature = SdirFeatureByOptionNumber(i);

        //
        //  If we're displaying or sorting, we need the data to use
        //

        if ((Feature->Flags & SDIR_FEATURE_COLLECT) &&
               SdirOptions[i].CollectFn) {

            SdirOptions[i].CollectFn(CurrentEntry, FindData, FullPath);
        }
    }

    //
    //  Determine the color to display each entry from extensions and attributes.
    //

    SdirApplyAttribute(CurrentEntry, ForceDisplay, &CurrentEntry->RenderAttributes);
}

/**
 Capture enough state about a file from its path to determine the color
 to display it with.  This is used when displaying directory names as part
 of recursive enumerations where the directories aren't rendered as part of
 the regular display but we still want to display them with color.

 @param FullPath A fully specified path to an object (which is currently a
        directory.)

 @param OutAttributes On completion, populated with the color attributes to
        use to display the object.
 */
VOID
SdirRenderAttributesFromPath (
    __in PYORI_STRING FullPath,
    __out PYORILIB_COLOR_ATTRIBUTES OutAttributes
    )
{
    HANDLE hFind;
    WIN32_FIND_DATA FindData;
    YORI_FILE_INFO CurrentEntry;

    hFind = FindFirstFile(FullPath->StartOfString, &FindData);
    if (hFind != INVALID_HANDLE_VALUE) {
        SdirCaptureFoundItemIntoDirent(&CurrentEntry, &FindData, FullPath, TRUE);
        FindClose(hFind);
        OutAttributes->Ctrl = CurrentEntry.RenderAttributes.Ctrl;
        OutAttributes->Win32Attr = CurrentEntry.RenderAttributes.Win32Attr;
        return;
    } else {
        YORI_STRING DummyString;

        if (!YoriLibAllocateString(&DummyString, FullPath->LengthInChars + 2)) {
            OutAttributes->Ctrl = SdirDefaultColor.Ctrl;
            OutAttributes->Win32Attr = SdirDefaultColor.Win32Attr;
            return;
        };

        memset(&FindData, 0, sizeof(FindData));
        DummyString.LengthInChars = YoriLibSPrintfS(DummyString.StartOfString, DummyString.LengthAllocated, _T("%s\\"), FullPath);
        YoriLibUpdateFindDataFromFileInformation(&FindData, DummyString.StartOfString, FALSE);
        SdirCaptureFoundItemIntoDirent(&CurrentEntry, &FindData, &DummyString, TRUE);
        YoriLibFreeStringContents(&DummyString);
        OutAttributes->Ctrl = CurrentEntry.RenderAttributes.Ctrl;
        OutAttributes->Win32Attr = CurrentEntry.RenderAttributes.Win32Attr;
        return;
    }
}

/**
 Add a single found object to the set of files found so far.

 @param FindData Pointer to the block of data returned from the directory as
        part of the enumeration.

 @param FullPath Pointer to a fully specified file name for the file.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SdirAddToCollection (
    __in PWIN32_FIND_DATA FindData,
    __in PYORI_STRING FullPath
    ) 
{
    PYORI_FILE_INFO CurrentEntry;
    DWORD i, j;
    DWORD CompareResult = 0;

    if (SdirDirCollectionCurrent >= SdirAllocatedDirents) {
        if (SdirDirCollectionCurrent < UINT_MAX) {
            SdirDirCollectionCurrent++;
        }
        return FALSE;
    }

    CurrentEntry = &SdirDirCollection[SdirDirCollectionCurrent];

    SdirDirCollectionCurrent++;

    SdirCaptureFoundItemIntoDirent(CurrentEntry, FindData, FullPath, FALSE);

    if (CurrentEntry->RenderAttributes.Ctrl & YORILIB_ATTRCTRL_HIDE) {

        SdirDirCollectionCurrent--;
        return TRUE;
    }

    if (CurrentEntry->FileNameLengthInChars > SdirDirCollectionLongest) {
        SdirDirCollectionLongest = CurrentEntry->FileNameLengthInChars;
    }

    SdirDirCollectionTotalNameLength += CurrentEntry->FileNameLengthInChars;

    if (Opts->FtSummary.Flags & SDIR_FEATURE_COLLECT) {
        SdirCollectSummary(CurrentEntry);
    }

    //
    //  Now that our internal entry is fully poulated, insert it into the
    //  correct sorted position.  As an optimization, check if we just need
    //  to insert at the end (for file name sort on NTFS, this is the
    //  common case.)
    //

    if (SdirDirCollectionCurrent > 1 &&
        Opts->Sort[0].CompareFn(SdirDirSorted[SdirDirCollectionCurrent - 2], CurrentEntry) == Opts->Sort[0].CompareInverseCondition) {

        SdirDirSorted[SdirDirCollectionCurrent - 1] = CurrentEntry;
        return TRUE;
    }

    //
    //  Currently sorting is done very inefficiently by a selection sort
    //  type algorithm.  Generally this doesn't run due to the optimization
    //  above.  When a different sort order is requested, performance could
    //  be improved by using a better algorithm here.
    //

    for (i = 0; i < SdirDirCollectionCurrent - 1; i++) {

        for (j = 0; j < Opts->CurrentSort; j++) {
            CompareResult = Opts->Sort[j].CompareFn(SdirDirSorted[i], CurrentEntry);

            if (CompareResult == Opts->Sort[j].CompareBreakCondition ||
                CompareResult == Opts->Sort[j].CompareInverseCondition) {
                break;
            }
        }

        if (CompareResult == Opts->Sort[j].CompareBreakCondition) {
            for (j = SdirDirCollectionCurrent - 1; j > i; j--) {
                SdirDirSorted[j] = SdirDirSorted[j - 1];
            }
            SdirDirSorted[i] = CurrentEntry;
            return TRUE;
        }
    }
    SdirDirSorted[SdirDirCollectionCurrent - 1] = CurrentEntry;
    return TRUE;
}

/**
 A context structure passed around through all files found as part of a single
 enumerate request.
 */
typedef struct _SDIR_ITEM_FOUND_CONTEXT {

    /**
     The number of items that were found as part of a single enumerate
     request.
     */
    DWORD ItemsFound;

    /**
     A Win32 error code, initialized to success and populated with an error
     if any enumeration operation fails.  This is done to reduce reliance
     on Win32 GetLastError, because it allows us to plumb an enumerate
     error back to the calling function without promising not to make
     Win32 calls in the meantime.
     */
    DWORD Error;

    /**
     An allocation for enumerating streams within files.  This is generally
     never used unless stream enumeration is requested.  The caller should
     always be prepared to free it.
     */
    YORI_STRING StreamFullPath;


} SDIR_ITEM_FOUND_CONTEXT, *PSDIR_ITEM_FOUND_CONTEXT;

/**
 A callback invoked by @ref SdirEnumeratePathWithDepth when an enumerate
 error occurs.

 @param FullPath Pointer to a full, escaped path to the file.

 @param ErrorCode Specifies the error that was encountered.

 @param Depth Specifies the recursion depth.  This should be zero and is
        ignored.

 @param Context Pointer to a an SDIR_ITEM_FOUND_CONTEXT block for all objects
        found via a single enumerate request.
 
 @return TRUE to indicate enumeration should continue, FALSE to indicate it
         should terminate.
 */
BOOL
SdirEnumerateErrorCallback(
    __in PYORI_STRING FullPath,
    __in DWORD ErrorCode,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    PSDIR_ITEM_FOUND_CONTEXT ItemContext = (PSDIR_ITEM_FOUND_CONTEXT)Context;

    UNREFERENCED_PARAMETER(FullPath);
    UNREFERENCED_PARAMETER(Depth);

    if (ItemContext->Error == ERROR_SUCCESS) {
        ItemContext->Error = ErrorCode;
    }

    return TRUE;
}


/**
 A callback invoked by @ref SdirEnumeratePathWithDepth for every file found.

 @param FullPath Pointer to a full, escaped path to the file.

 @param FindData Points to information returned from the directory enumerate.

 @param Depth Specifies the recursion depth.  This should be zero and is
        ignored.

 @param Context Pointer to a an SDIR_ITEM_FOUND_CONTEXT block for all objects
        found via a single enumerate request.
 
 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SdirItemFoundCallback(
    __in PYORI_STRING FullPath,
    __in PWIN32_FIND_DATA FindData,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    PSDIR_ITEM_FOUND_CONTEXT ItemContext = (PSDIR_ITEM_FOUND_CONTEXT)Context;

    UNREFERENCED_PARAMETER(Depth);

#if defined(UNICODE)
    if (DllKernel32.pFindFirstStreamW != NULL &&
        (Opts->FtNamedStreams.Flags & SDIR_FEATURE_DISPLAY)) {

        HANDLE hStreamFind;
        WIN32_FIND_STREAM_DATA FindStreamData;
        WIN32_FIND_DATA BogusFindData;

        //
        //  Display the default stream
        //

        SdirAddToCollection(FindData, FullPath);

        //
        //  Look for any named streams
        //

        hStreamFind = DllKernel32.pFindFirstStreamW(FullPath->StartOfString, 0, &FindStreamData, 0);
        if (hStreamFind != INVALID_HANDLE_VALUE) {

            //
            //  If the existing buffer isn't large enough, allocate a new
            //  one.  Add in an extra 100 chars in the hope that this new
            //  buffer can be reused for a later file.
            //

            if (ItemContext->StreamFullPath.LengthAllocated < FullPath->LengthInChars + YORI_LIB_MAX_STREAM_NAME) {
                YoriLibFreeStringContents(&ItemContext->StreamFullPath);
                if (!YoriLibAllocateString(&ItemContext->StreamFullPath, FullPath->LengthInChars + YORI_LIB_MAX_STREAM_NAME + 100)) {
                    FindClose(hStreamFind);
                    return FALSE;
                }
            }

            do {

                //
                //  For the default stream, just report the information we found
                //  for the file.  For anything else, query all the equivalent
                //  stream information
                //

                if (_tcscmp(FindStreamData.cStreamName, L"::$DATA") != 0) {

                    DWORD StreamLength = (DWORD)_tcslen(FindStreamData.cStreamName);

                    if (StreamLength > 6 && _tcscmp(FindStreamData.cStreamName + StreamLength - 6, L":$DATA") == 0) {
                        FindStreamData.cStreamName[StreamLength - 6] = '\0';
                    }

                    ItemContext->StreamFullPath.LengthInChars = YoriLibSPrintfS(ItemContext->StreamFullPath.StartOfString, ItemContext->StreamFullPath.LengthAllocated, _T("%s%s%s"), Opts->ParentName.StartOfString, FindData->cFileName, FindStreamData.cStreamName);

                    //
                    //  Assume file state is stream state
                    //

                    memcpy(&BogusFindData, &FindData, sizeof(FindData));

                    //
                    //  Populate stream name
                    //

                    YoriLibSPrintfS(BogusFindData.cFileName, sizeof(BogusFindData.cFileName)/sizeof(BogusFindData.cFileName[0]), _T("%s%s"), FindData->cFileName, FindStreamData.cStreamName);

                    //
                    //  Populate stream information
                    //

                    if (!YoriLibUpdateFindDataFromFileInformation(&BogusFindData, ItemContext->StreamFullPath.StartOfString, FALSE)) {
                        memcpy(&BogusFindData, &FindData, sizeof(FindData));
                    }
                    SdirAddToCollection(&BogusFindData, &ItemContext->StreamFullPath);
                }
            } while (DllKernel32.pFindNextStreamW(hStreamFind, &FindStreamData));
        }

        FindClose(hStreamFind);

    } else {
#endif
        SdirAddToCollection(FindData, FullPath);
#if defined(UNICODE)
    }
#endif
    ItemContext->ItemsFound++;
    return TRUE;
}

/**
 Copy entries from one old sorted array allocation to a new allocation.
 When this occurs the pointer values in the old array need to be adjusted
 from the old allocation to the new allocation.  In addition, entries may
 have been inserted into the old collection which should not be preserved
 because they will be reenumerated.  When this occurs they will be in the
 sorted array in sorted order, so any entry beyond the end of the
 collection array should not be propagated to the new sort array.

 @param OldCollection Pointer to the previous array of directory entries.

 @param NewCollection Pointer to the new array of directory entries.  This,
        combined with OldCollection, implies the offset adjustment to apply
        to any entry within the sorted array.

 @param NumberEntriesToCopy Specifies the maximum number of entries that
        should be copied into the new sorted array.  This is the number of
        entries within the collection that are being preserved; newer entries
        will be enumerated again in a subsequent operation.

 @param OldSorted Pointer to the previous array of sorted entries.  Entries
        within this array will be filtered against NumberEntriesToCopy,
        adjusted based on pointer offset, and propagated to the new array.

 @param NewSorted Pointer to the new array of sorted entries.  This will be
        populated within this routine.

 @param NumberSortedEntries The number of elements in the OldSorted array.
 */
VOID
SdirMoveSortedEntries(
    __in PYORI_FILE_INFO OldCollection,
    __in PYORI_FILE_INFO NewCollection,
    __in DWORD NumberEntriesToCopy,
    __in_ecount(NumberSortedEntries) PYORI_FILE_INFO* OldSorted,
    __out_ecount(NumberEntriesToCopy) PYORI_FILE_INFO* NewSorted,
    __in DWORD NumberSortedEntries
    )
{
    DWORD SrcIndex;
    DWORD DestIndex;
    PYORI_FILE_INFO ThisEntry;

    DestIndex = 0;
    for (SrcIndex = 0; SrcIndex < NumberSortedEntries; SrcIndex++) {
        ThisEntry = (PYORI_FILE_INFO)OldSorted[SrcIndex];
        ASSERT(ThisEntry >= OldCollection);
        if (ThisEntry < &OldCollection[NumberEntriesToCopy]) {
            ASSERT(DestIndex < NumberEntriesToCopy);
            if (DestIndex >= NumberEntriesToCopy) {
                break;
            }
            NewSorted[DestIndex] = (PYORI_FILE_INFO)((PUCHAR)ThisEntry -
                                                     (PUCHAR)OldCollection +
                                                     (PUCHAR)NewCollection);
            DestIndex++;
        }
    }

    ASSERT(DestIndex == NumberEntriesToCopy);
}


/**
 Enumerate all of the files in a given single directory/wildcard pattern,
 and populate the results into the global SdirAllocatedDirents array.

 @param FindStr The compound directory/wildcard pattern to enumerate.

 @param Depth Recursion depth.  If nonzero, this implies basic enumeration.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SdirEnumeratePathWithDepth (
    __in PYORI_STRING FindStr,
    __in DWORD Depth
    )
{
    LPTSTR FinalPart;
    ULONG DirEntsToPreserve;
    SDIR_SUMMARY SummaryToPreserve;
    PYORI_FILE_INFO NewSdirDirCollection;
    PYORI_FILE_INFO * NewSdirDirSorted;
    SDIR_ITEM_FOUND_CONTEXT ItemFoundContext;
    DWORD MatchFlags;

    //
    //  At this point we should have a directory and an enumeration criteria.
    //  Copy the directory name and clobber the enumeration criteria.
    //

    if (Opts->ParentName.MemoryToFree != NULL) {
        YoriLibFreeStringContents(&Opts->ParentName);
    }

    if (!YoriLibGetFullPathNameReturnAllocation(FindStr, TRUE, &Opts->ParentName, &FinalPart)) {
        SdirDisplayError(GetLastError(), _T("YoriLibGetFullPathNameReturnAllocation"));
        return FALSE;
    }

    if (FinalPart) {
        Opts->ParentName.LengthInChars = (DWORD)(FinalPart - Opts->ParentName.StartOfString);
        *FinalPart = '\0';
    }

    if (Opts->FtSummary.Flags & SDIR_FEATURE_COLLECT) {

        LARGE_INTEGER Junk;

        //
        //  If GetDiskFreeSpaceEx fails, fall back to GetDiskFreeSpace.  According
        //  to MSDN, this can sometimes be present and fail with not supported.
        //

        if (Summary->VolumeSize.QuadPart == 0 &&
            (DllKernel32.pGetDiskFreeSpaceExW == NULL ||
             !DllKernel32.pGetDiskFreeSpaceExW(Opts->ParentName.StartOfString, &Junk, &Summary->VolumeSize, &Summary->FreeSize))) {

            if (!SdirPopulateSummaryWithGetDiskFreeSpace(Opts->ParentName.StartOfString, Summary)) {

                //
                //  On very old platforms, this API requires a volume root.
                //  Frustratingly, that also means the APIs to detect a volume
                //  root don't exist, so all we can do is guess.  This code
                //  currently guesses for drive letters but not SMB shares;
                //  (my patience for NT 3.x support is not limitless.)
                //

                TCHAR BackupChar;
                DWORD VolumeRootLength = 0;

                if (VolumeRootLength == 0) {

                    if (Opts->ParentName.StartOfString[0] == '\\' &&
                        Opts->ParentName.StartOfString[1] == '\\' &&
                        (Opts->ParentName.StartOfString[2] == '?' || Opts->ParentName.StartOfString[2] == '.') &&
                        Opts->ParentName.StartOfString[3] == '\\' &&
                        Opts->ParentName.StartOfString[5] == ':' &&
                        Opts->ParentName.StartOfString[6] == '\\') {

                        VolumeRootLength = 7;

                    } else if (Opts->ParentName.StartOfString[1] == ':' &&
                               Opts->ParentName.StartOfString[2] == '\\') {

                        VolumeRootLength = 3;
                    }
                }

                if (VolumeRootLength) {
                    BackupChar = Opts->ParentName.StartOfString[VolumeRootLength];
                    Opts->ParentName.StartOfString[VolumeRootLength] = '\0';

                    SdirPopulateSummaryWithGetDiskFreeSpace(Opts->ParentName.StartOfString, Summary);

                    Opts->ParentName.StartOfString[VolumeRootLength] = BackupChar;
                }
            }
        }
    }

    //
    //  We loop enumerating all the files.  Hopefully for common directories
    //  we'll allocate a big enough buffer in the first case and we can then
    //  just populate that buffer and display it.  If the directory is large
    //  enough though, we count the number of entries, loop back, and allocate
    //  a large enough buffer to hold the result, then enumerate it again.
    //  Because we sort the output, we must keep the entire set in memory to
    //  be able to meaningfully process it.
    //

    DirEntsToPreserve = SdirDirCollectionCurrent;
    memcpy(&SummaryToPreserve, Summary, sizeof(SummaryToPreserve));

    do {

        //
        //  If this is a subsequent pass and we didn't allocate a big enough
        //  buffer, reallocate our buffers.  Add an extra 100 just in case
        //  we're still adding files in real time.
        //

        if (SdirDirCollectionCurrent >= SdirAllocatedDirents || SdirDirCollection == NULL) {
            DWORD PreviousAllocatedDirents = SdirAllocatedDirents;

            if (SdirDirCollectionCurrent >= SdirAllocatedDirents) {
                SdirAllocatedDirents = SdirDirCollectionCurrent + 1;
            }
            if (SdirAllocatedDirents < UINT_MAX - 100) {
                SdirAllocatedDirents += 100;
            }

            NewSdirDirCollection = YoriLibMalloc(SdirAllocatedDirents * sizeof(YORI_FILE_INFO));
            if (NewSdirDirCollection == NULL) {
                SdirAllocatedDirents = SdirDirCollectionCurrent;
                SdirDisplayError(GetLastError(), _T("YoriLibMalloc"));
                return FALSE;
            }

            NewSdirDirSorted = YoriLibMalloc(SdirAllocatedDirents * sizeof(PYORI_FILE_INFO));
            if (NewSdirDirSorted == NULL) {
                SdirAllocatedDirents = SdirDirCollectionCurrent;
                YoriLibFree(NewSdirDirCollection);
                SdirDisplayError(GetLastError(), _T("YoriLibMalloc"));
                return FALSE;
            }

            //
            //  Copy back any previous data.  This occurs when multiple
            //  criteria are specified, eg., "*.a *.b".  Apply fixups to
            //  everything in the sorted array which were based on the
            //  previous collection and now need to be based on the new one.
            //

            if (DirEntsToPreserve > 0 && SdirDirCollection != NULL) {
                memcpy(NewSdirDirCollection, SdirDirCollection, sizeof(YORI_FILE_INFO)*DirEntsToPreserve);
                SdirMoveSortedEntries(SdirDirCollection, NewSdirDirCollection, DirEntsToPreserve, SdirDirSorted, NewSdirDirSorted, PreviousAllocatedDirents);
            }

            if (SdirDirCollection != NULL) {
                YoriLibFree(SdirDirCollection);
            }

            if (SdirDirSorted != NULL) {
                YoriLibFree(SdirDirSorted);
            }

            SdirDirCollection = NewSdirDirCollection;
            SdirDirSorted = NewSdirDirSorted;
            SdirDirCollectionCurrent = DirEntsToPreserve;
            memcpy(Summary, &SummaryToPreserve, sizeof(SummaryToPreserve));
        }

        //
        //  If we can't find enumerate, display the error except when we're recursive
        //  and the error is we found no files in this particular directory.
        //

        ItemFoundContext.ItemsFound = 0;
        MatchFlags = YORILIB_FILEENUM_RETURN_FILES | YORILIB_FILEENUM_RETURN_DIRECTORIES | YORILIB_FILEENUM_INCLUDE_DOTFILES;

        //
        //  MSFIX This isn't really correct without a major refactor.  What
        //  we want is to allow full expansion of the search criteria but
        //  basic expansion of the search path, since it was the result of
        //  a prior enumerate.
        //

        if (Depth > 0 || Opts->BasicEnumeration) {
            MatchFlags |= YORILIB_FILEENUM_BASIC_EXPANSION;
        }

        YoriLibInitEmptyString(&ItemFoundContext.StreamFullPath);
        ItemFoundContext.Error = ERROR_SUCCESS;

        if (!YoriLibForEachFile(FindStr,
                                MatchFlags,
                                0,
                                SdirItemFoundCallback,
                                SdirEnumerateErrorCallback,
                                &ItemFoundContext)) {

            if (!Opts->Recursive) {
                if (ItemFoundContext.Error == ERROR_SUCCESS) {
                    ItemFoundContext.Error = GetLastError();
                }
                YoriLibFreeStringContents(&ItemFoundContext.StreamFullPath);

                //
                //  For file not found errors, continue enumerating through
                //  all of the criteria specified by the user, and display
                //  it only if there are no files from any criteria
                //

                if (ItemFoundContext.Error != ERROR_FILE_NOT_FOUND) {
                    SdirDisplayYsError(ItemFoundContext.Error, FindStr);
                    SetLastError(ItemFoundContext.Error);
                } else {
                    return TRUE;
                }
            } else {
                YoriLibFreeStringContents(&ItemFoundContext.StreamFullPath);
            }
            return FALSE;
        }

        YoriLibFreeStringContents(&ItemFoundContext.StreamFullPath);

        if (ItemFoundContext.ItemsFound == 0) {
            if (!Opts->Recursive) {
                if (ItemFoundContext.Error == ERROR_SUCCESS) {
                    ItemFoundContext.Error = ERROR_FILE_NOT_FOUND;
                }

                if (ItemFoundContext.Error != ERROR_FILE_NOT_FOUND) {
                    SdirDisplayYsError(ItemFoundContext.Error, FindStr);
                } else {
                    return TRUE;
                }
            }
            SetLastError(ERROR_FILE_NOT_FOUND);
            return FALSE;
        }

        //
        //  If we've encountered more than we've allocated, go back
        //  and reallocate.
        //

    } while (SdirDirCollectionCurrent >= SdirAllocatedDirents);

    return TRUE;
}

/**
 Enumerate all of the files in a given single directory/wildcard pattern,
 and populate the results into the global SdirAllocatedDirents array.
 This is a trivial wrapper around SdirEnumeratePathWithDepth to maintain
 a function signature.

 @param FindStr The compound directory/wildcard pattern to enumerate.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SdirEnumeratePath (
    __in PYORI_STRING FindStr
    )
{
    return SdirEnumeratePathWithDepth(FindStr, 0);
}


/**
 An array element in the character array corresponding to a horizontal line.
 */
#define SDIR_LINE_ELEMENT_HORIZ      0

/**
 An array element in the character array corresponding to a horizontal and
 vertical line intersection where the vertical line is beneath the horizontal
 line.
 */
#define SDIR_LINE_ELEMENT_TOP_T      1

/**
 An array element in the character array corresponding to a horizontal and
 vertical line intersection where the vertical line is above the horizontal
 line.
 */
#define SDIR_LINE_ELEMENT_BOTTOM_T   2

/**
 An array element in the character array corresponding to a vertical line.
 */
#define SDIR_LINE_ELEMENT_VERT       3

#ifdef UNICODE
/**
 A character array for grid characters using Unicode characters.
 */
TCHAR SdirLineElementsRich[] = {0x2500, 0x252c, 0x2534, 0x2502};
#endif
/**
 A character array for grid characters using only 7 bit characters.
 */
TCHAR SdirLineElementsText[] = {'-', '+', '+', '|'};


/**
 Write a newline by generating the active line ending characters and sending
 them to the display module.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SdirNewlineThroughDisplay(VOID)
{
    SDIR_FMTCHAR Line;

    Line.Char = '\n';
    Line.Attr.Ctrl = SdirDefaultColor.Ctrl;
    Line.Attr.Win32Attr = SdirDefaultColor.Win32Attr;

    SdirWrite(&Line, 1);
    return TRUE;
}

/**
 Resolves a specific feature color either from a configured value for that
 feature or from the file's color if the feature color was supposed to be
 derived from that.
 */
VOID
SdirFeatureColor(
    __in PSDIR_FEATURE Feature,
    __in YORILIB_COLOR_ATTRIBUTES FileColor,
    __out PYORILIB_COLOR_ATTRIBUTES OutColor
    )
{
    if (Feature->Flags & SDIR_FEATURE_USE_FILE_COLOR) {
        OutColor->Ctrl = FileColor.Ctrl;
        OutColor->Win32Attr = FileColor.Win32Attr;
    } else {
        OutColor->Ctrl = Feature->HighlightColor.Ctrl;
        OutColor->Win32Attr = Feature->HighlightColor.Win32Attr;
    }
}


/**
 Display the loaded set of files.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SdirDisplayCollection(VOID)
{
    PYORI_FILE_INFO CurrentEntry;
    DWORD Index, Ext;
    YORILIB_COLOR_ATTRIBUTES Attributes;
    YORILIB_COLOR_ATTRIBUTES FeatureColor;
    DWORD Columns;
    DWORD ColumnWidth;
    DWORD ActiveColumn = 0;
    SDIR_FMTCHAR Line[SDIR_MAX_WIDTH];
    DWORD CurrentChar = 0;
    DWORD BufferRows;
    DWORD LongestDisplayedFileName = SdirDirCollectionLongest;
    LPTSTR LineElements = SdirLineElementsText;
    PSDIR_FEATURE Feature;

#ifdef UNICODE
    if (Opts->OutputExtendedCharacters) {
        LineElements = SdirLineElementsRich;
    }
#endif

    //
    //  If we're allowed to shorten names to make the display more
    //  legible, we won't allow a longest name greater than twice
    //  average size.  Since we need to insert three characters to
    //  indicate we butchered a name, don't do this unless we have
    //  a meaningful length to start with (currently 10.)
    //

    if (Opts->EnableNameTruncation) {
        ULONG AverageNameLength;

        AverageNameLength = SdirDirCollectionTotalNameLength / SdirDirCollectionCurrent;
        if (LongestDisplayedFileName > 2 * AverageNameLength) {
            LongestDisplayedFileName = 2 * AverageNameLength;
            if (LongestDisplayedFileName < 10) {
                LongestDisplayedFileName = 10;
            }
        }
    }

    ColumnWidth = Opts->ConsoleWidth;
    if (ColumnWidth > SDIR_MAX_WIDTH) {
        ColumnWidth = SDIR_MAX_WIDTH;
    }

    if (Opts->FtFileName.Flags & SDIR_FEATURE_DISPLAY) {
        Columns = ColumnWidth / (LongestDisplayedFileName + Opts->MetadataWidth);
    } else {
        Columns = ColumnWidth / (Opts->MetadataWidth);
    }

    //
    //  If the output is too big to fit the display, force one column
    //  of sufficient size.  The output here will be scrambled, but it
    //  will indicate to the user what happened.
    //

    if (Columns > 0) {
        ColumnWidth = ColumnWidth / Columns;
        LongestDisplayedFileName = ColumnWidth - Opts->MetadataWidth;
    } else {
        Columns = 1;
        ColumnWidth = Opts->MetadataWidth;
        if (Opts->FtFileName.Flags & SDIR_FEATURE_DISPLAY) {
            ColumnWidth += LongestDisplayedFileName;
        }
    }

    //
    //  This really shouldn't happen, even in the worst case of a MAX_PATH name
    //  with all metadata options enabled, but we'll be paranoid.
    //

    if (Columns * ColumnWidth > SDIR_MAX_WIDTH) {
        SdirWriteString(_T("Too much data for a single line!"));
        return FALSE;
    }

    BufferRows = (SdirDirCollectionCurrent + Columns - 1) / Columns;

    //
    //  Draw the top grid line.
    //

    for (Index = 0; Index < ColumnWidth * Columns; Index++) {
        if (Index % ColumnWidth == ColumnWidth - 1 && Index < (ColumnWidth*Columns-1)) {
            Line[Index].Char = LineElements[SDIR_LINE_ELEMENT_TOP_T];
        } else {
            Line[Index].Char = LineElements[SDIR_LINE_ELEMENT_HORIZ];
        }
        Line[Index].Attr.Ctrl = Opts->FtGrid.HighlightColor.Ctrl;
        Line[Index].Attr.Win32Attr = Opts->FtGrid.HighlightColor.Win32Attr;
    }

    SdirWrite(Line, Index);

    if (ColumnWidth * Columns != Opts->ConsoleBufferWidth || !Opts->OutputHasAutoLineWrap) {
        SdirNewlineThroughDisplay();
    }

    if (!SdirRowDisplayed()) {
        return FALSE;
    }

    //
    //  Enumerate through the entries.
    //

    for (Index = 0; Index < BufferRows * Columns && !Opts->Cancelled; Index++) {

        //
        //  Because we're sorting down columns first, but rendering a row at a time,
        //  we need to do some matrix math to find which elements belong in which cells.
        //  Some cells in the bottom right might be empty.
        //

        Ext = ActiveColumn * BufferRows + Index / Columns;
        if (Ext < SdirDirCollectionCurrent) {
            CurrentEntry = SdirDirSorted[Ext];
        } else {
            CurrentEntry = NULL;
        }

        //
        //  Render the empty cell.  Or, if we have contents, render that too.
        //

        if (CurrentEntry == NULL) {
            SdirPasteStrAndPad(&Line[CurrentChar], NULL, SdirDefaultColor, 0, ColumnWidth - 1);
            CurrentChar += ColumnWidth - 1;
        } else {

            Attributes.Ctrl = CurrentEntry->RenderAttributes.Ctrl;
            Attributes.Win32Attr = CurrentEntry->RenderAttributes.Win32Attr;

            //
            //  Paste file name into buffer
            //

            if (Opts->FtFileName.Flags & SDIR_FEATURE_DISPLAY) {
                SdirFeatureColor(&Opts->FtFileName, Attributes, &FeatureColor);
                if (CurrentEntry->FileNameLengthInChars > LongestDisplayedFileName) {
                    ULONG ExtractedLength = (LongestDisplayedFileName - 3) / 2;

                    SdirPasteStr(&Line[CurrentChar], CurrentEntry->FileName, Attributes, ExtractedLength);
                    CurrentChar += ExtractedLength;

                    SdirPasteStr(&Line[CurrentChar], _T("..."), Attributes, ExtractedLength);
                    CurrentChar += 3;

                    SdirPasteStrAndPad(&Line[CurrentChar],
                                       CurrentEntry->FileName + CurrentEntry->FileNameLengthInChars - ExtractedLength,
                                       FeatureColor,
                                       ExtractedLength,
                                       ColumnWidth - Opts->MetadataWidth - ExtractedLength - 3);

                    CurrentChar += ColumnWidth - Opts->MetadataWidth - ExtractedLength - 3;
                } else {
                    SdirPasteStrAndPad(&Line[CurrentChar],
                                       CurrentEntry->FileName,
                                       FeatureColor,
                                       CurrentEntry->FileNameLengthInChars,
                                       ColumnWidth - Opts->MetadataWidth);
                    CurrentChar += ColumnWidth - Opts->MetadataWidth;
                }
            }

            if (Opts->FtShortName.Flags & SDIR_FEATURE_DISPLAY) {
                SdirFeatureColor(&Opts->FtShortName, Attributes, &FeatureColor);
                CurrentChar += SdirDisplayShortName(&Line[CurrentChar], FeatureColor, CurrentEntry);

                if (!(Opts->FtFileName.Flags & SDIR_FEATURE_DISPLAY)) {

                    //
                    //  If file name is hidden, we may need to align things, because
                    //  our columns may contain padding (they're essentially justified.)
                    //

                    SdirPasteStrAndPad(&Line[CurrentChar], NULL, FeatureColor, 0, ColumnWidth - Opts->MetadataWidth);
                    CurrentChar += ColumnWidth - Opts->MetadataWidth;
                }
            }

            //
            //  If file names or short names are being displayed, column
            //  justification has already been performed.  If neither
            //  are displayed, force manual justification here.
            //

            if (!(Opts->FtShortName.Flags & SDIR_FEATURE_DISPLAY || Opts->FtFileName.Flags & SDIR_FEATURE_DISPLAY)) {
                SdirPasteStrAndPad(&Line[CurrentChar],
                                   NULL,
                                   Attributes,
                                   0,
                                   ColumnWidth - Opts->MetadataWidth);
                CurrentChar += ColumnWidth - Opts->MetadataWidth;
            }

            //
            //  Paste any metadata options into the buffer.
            //

            for (Ext = 0; Ext < SdirGetNumSdirExec(); Ext++) {
                Feature = (PSDIR_FEATURE)((PUCHAR)Opts + SdirExec[Ext].FtOffset);
                if (Feature->Flags & SDIR_FEATURE_DISPLAY) {
                    SdirFeatureColor(Feature, Attributes, &FeatureColor);
                    CurrentChar += SdirExec[Ext].Function(&Line[CurrentChar], FeatureColor, CurrentEntry);
                }
            }
        }

        //
        //  We're starting a new column.  If it's the final one we might want a newline,
        //  otherwise we might want a gridline.  Do this manually so we only write the
        //  line once.
        //

        ActiveColumn++;
        if (ActiveColumn % Columns == 0) {

            Line[CurrentChar].Char = '\n';
            Line[CurrentChar].Attr.Ctrl = SdirDefaultColor.Ctrl;
            Line[CurrentChar].Attr.Win32Attr = SdirDefaultColor.Win32Attr;
            CurrentChar++;
            SdirWrite(Line, CurrentChar);

            CurrentChar = 0;
            ActiveColumn = 0;
            if (!SdirRowDisplayed()) {
                return FALSE;
            }
        } else {
            Line[CurrentChar].Char = LineElements[SDIR_LINE_ELEMENT_VERT];
            Line[CurrentChar].Attr.Ctrl = Opts->FtGrid.HighlightColor.Ctrl;
            Line[CurrentChar].Attr.Win32Attr = Opts->FtGrid.HighlightColor.Win32Attr;
            CurrentChar++;
        }
    }

    //
    //  Render the bottom gridline.
    //

    for (Index = 0; Index < ColumnWidth * Columns; Index++) {
        if (Index % ColumnWidth == ColumnWidth - 1 && Index < (ColumnWidth*Columns-1)) {
            Line[Index].Char = LineElements[SDIR_LINE_ELEMENT_BOTTOM_T];
        } else {
            Line[Index].Char = LineElements[SDIR_LINE_ELEMENT_HORIZ];
        }
        Line[Index].Attr.Ctrl = Opts->FtGrid.HighlightColor.Ctrl;
        Line[Index].Attr.Win32Attr = Opts->FtGrid.HighlightColor.Win32Attr;
    }

    SdirWrite(Line, Index);
    CurrentChar = 0;

    if (ColumnWidth * Columns != Opts->ConsoleBufferWidth || !Opts->OutputHasAutoLineWrap) {
        SdirNewlineThroughDisplay();
        CurrentChar = 0;
    }

    if (!SdirRowDisplayed()) {
        return FALSE;
    }

    return TRUE;
}

/**
 A prototype for a callback function to invoke for each parameter that
 describes a set of files to enumerate.
 */
typedef BOOL (* SDIR_FOR_EACH_PATHSPEC_FN)(PYORI_STRING);

/**
 For every parameter specified on the command line that refers to a set of
 files, invoke a callback to facilitate enumeration.  If the user hasn't
 specified any set of files explicitly, enumerate all files from the current
 directory.

 @param ArgC The number of arguments passed to the application.

 @param ArgV An array of arguments passed to the application.

 @param Callback Specifies a callback function to invoke for each parameter
        that represents a set of files.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SdirForEachPathSpec (
    __in DWORD ArgC,
    __in YORI_STRING ArgV[],
    __in SDIR_FOR_EACH_PATHSPEC_FN Callback
    )
{
    ULONG  CurrentArg;
    BOOLEAN EnumerateUserSpecified = FALSE;
    YORI_STRING FindStr;
    YORI_STRING Arg;

    YoriLibInitEmptyString(&FindStr);

    for (CurrentArg = 1; CurrentArg < ArgC; CurrentArg++) {

        if (!YoriLibIsCommandLineOption(&ArgV[CurrentArg], &Arg)) {

            HANDLE hDir;

            EnumerateUserSpecified = TRUE;

            YoriLibFreeStringContents(&FindStr);

            if (!YoriLibUserStringToSingleFilePath(&ArgV[CurrentArg], TRUE, &FindStr)) {
                SdirDisplayError(GetLastError(), _T("YoriLibUserStringToSingleFilePath"));
                return FALSE;
            }

            //
            //  FILE_READ_DATA (aka FILE_LIST_DIRECTORY) is needed for some
            //  popular SMB servers to return accurate information.  But if
            //  we don't have access and the info may be available, try
            //  without it.
            //

            hDir = CreateFile(FindStr.StartOfString,
                              FILE_READ_ATTRIBUTES|FILE_READ_DATA,
                              FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
                              NULL,
                              OPEN_EXISTING,
                              FILE_FLAG_BACKUP_SEMANTICS|FILE_FLAG_OPEN_REPARSE_POINT|FILE_FLAG_OPEN_NO_RECALL,
                              NULL);

            if (hDir == INVALID_HANDLE_VALUE && GetLastError() == ERROR_ACCESS_DENIED) {
                hDir = CreateFile(FindStr.StartOfString,
                                  FILE_READ_ATTRIBUTES,
                                  FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
                                  NULL,
                                  OPEN_EXISTING,
                                  FILE_FLAG_BACKUP_SEMANTICS|FILE_FLAG_OPEN_REPARSE_POINT|FILE_FLAG_OPEN_NO_RECALL,
                                  NULL);
            }

            if (hDir != INVALID_HANDLE_VALUE) {
                BY_HANDLE_FILE_INFORMATION HandleInfo;

                if (!GetFileInformationByHandle(hDir, &HandleInfo)) {
                    HandleInfo.dwFileAttributes = 0;
                }

                if (HandleInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
                    LPTSTR FindStrWithWild;
                    LPTSTR szFormatStr;
                    DWORD FindStrWithWildLength = (DWORD)FindStr.LengthInChars + 3;

                    FindStrWithWild = YoriLibReferencedMalloc(FindStrWithWildLength * sizeof(TCHAR));
                    if (FindStrWithWild == NULL) {
                        SdirDisplayError(GetLastError(), _T("YoriLibMalloc"));
                        YoriLibFreeStringContents(&FindStr);
                        return FALSE;
                    }
                    if (FindStr.StartOfString[FindStrWithWildLength - 4] == '\\') {
                        szFormatStr = _T("%y*");
                    } else {
                        szFormatStr = _T("%y\\*");
                    }
                    FindStr.LengthInChars = YoriLibSPrintfS(FindStrWithWild, FindStrWithWildLength, szFormatStr, &FindStr);
                    YoriLibDereference(FindStr.MemoryToFree);
                    FindStr.MemoryToFree = FindStrWithWild;
                    FindStr.StartOfString = FindStrWithWild;
                    FindStr.LengthAllocated = FindStr.LengthInChars + 1;
                }

                CloseHandle(hDir);
            }

            if (!Callback(&FindStr)) {
                YoriLibFreeStringContents(&FindStr);
                return FALSE;
            }
        }
    }

    YoriLibFreeStringContents(&FindStr);

    if (!EnumerateUserSpecified) {
        YoriLibConstantString(&FindStr, _T("*"));
        if (!Callback(&FindStr)) {
            return FALSE;
        }
    }

    return TRUE;
}

/**
 Enumerate and display the contents of a single directory.

 @param ArgC The number of arguments passed to the application.

 @param ArgV An array of arguments passed to the application.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SdirEnumerateAndDisplay (
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    if (!SdirForEachPathSpec(ArgC, ArgV, SdirEnumeratePath)) {
        return FALSE;
    }

    if (SdirDirCollectionCurrent == 0) {
        SdirDisplayError(ERROR_FILE_NOT_FOUND, NULL);
        return FALSE;
    }

    if (!SdirDisplayCollection()) {
        return FALSE;
    }

    return TRUE;
}

/**
 Error codes that should allow enumeration to continue rather than abort.
 */
#define SdirContinuableError(err) \
    (err == ERROR_FILE_NOT_FOUND || err == ERROR_ACCESS_DENIED)

/**
 Error codes that should be reported to the user when performing recursive
 enumerates.
 */
#define SdirIsReportableError(err) \
    (err != ERROR_FILE_NOT_FOUND)

/**
 Perform a recursive enumerate.

 @param Depth Indicates the current recursion depth.  The user specified
        directory is zero.

 @param FileSpec A string which contains a directory and search criteria.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SdirEnumerateAndDisplaySubtree (
    __in DWORD Depth,
    __in PYORI_STRING FileSpec
    )
{
    YORI_STRING ParentDirectory;
    YORI_STRING SearchCriteria;
    LPTSTR FinalBackslash;
    YORI_STRING NextSubDir;
    HANDLE hFind;
    WIN32_FIND_DATA FindData;
    SDIR_SUMMARY SummaryOnEntry;
    LPTSTR szFormatStr;

    YoriLibInitEmptyString(&ParentDirectory);
    YoriLibInitEmptyString(&SearchCriteria);

    FinalBackslash = YoriLibFindRightMostCharacter(FileSpec, '\\');
    if (FinalBackslash != NULL) {
        ParentDirectory.StartOfString = FileSpec->StartOfString;
        ParentDirectory.LengthInChars = (DWORD)(FinalBackslash - FileSpec->StartOfString);
        ParentDirectory.LengthAllocated = ParentDirectory.LengthInChars + 1;

        FinalBackslash[0] = '\0';

        SearchCriteria.StartOfString = FinalBackslash + 1;
        SearchCriteria.LengthInChars = FileSpec->LengthInChars - ParentDirectory.LengthInChars - 1;
        SearchCriteria.LengthAllocated = FileSpec->LengthAllocated - ParentDirectory.LengthAllocated;

    } else {
        SearchCriteria.StartOfString = FileSpec->StartOfString;
        SearchCriteria.LengthInChars = FileSpec->LengthInChars;
        SearchCriteria.LengthAllocated = FileSpec->LengthAllocated;
    }

    ASSERT(ParentDirectory.LengthInChars == 0 || YoriLibIsStringNullTerminated(&ParentDirectory));
    ASSERT(YoriLibIsStringNullTerminated(&SearchCriteria));

    memcpy(&SummaryOnEntry, Summary, sizeof(SDIR_SUMMARY));

    if (!YoriLibAllocateString(&NextSubDir, ParentDirectory.LengthInChars + YORI_LIB_MAX_FILE_NAME + 2 + SearchCriteria.LengthInChars + 1)) {
        SdirDisplayError(GetLastError(), _T("YoriLibAllocateString"));
        return FALSE;
    }

    if (ParentDirectory.LengthInChars == 0 ||
        ParentDirectory.StartOfString[ParentDirectory.LengthInChars - 1] == '\\') {
        szFormatStr = _T("%y%y");
    } else {
        szFormatStr = _T("%y\\%y");
    }

    if (YoriLibYPrintf(&NextSubDir, szFormatStr, &ParentDirectory, &SearchCriteria) < 0 ||
        NextSubDir.LengthInChars == 0) {

        SdirWriteString(_T("Path exceeds allocated length\n"));
        YoriLibFreeStringContents(&NextSubDir);
        return FALSE;
    }

    if (!SdirEnumeratePathWithDepth(&NextSubDir, Depth)) {
        DWORD Err = GetLastError();
        if (SdirIsReportableError(Err)) {

            Opts->ErrorsFound = TRUE;
        }
        if (!SdirContinuableError(Err)) {

            YoriLibFreeStringContents(&NextSubDir);
            return FALSE;
        }
    }

    //
    //  If we have something to display, display it.
    //

    if (SdirDirCollectionCurrent > 0) {

        YORILIB_COLOR_ATTRIBUTES RenderAttributes;
        SdirRenderAttributesFromPath(&ParentDirectory, &RenderAttributes);

        if (YoriLibIsFullPathUnc(&ParentDirectory)) {
            SdirWriteStringWithAttribute(_T("\\\\"), RenderAttributes);
            SdirWriteStringWithAttribute(&ParentDirectory.StartOfString[8], RenderAttributes);
        } else {
            SdirWriteStringWithAttribute(&ParentDirectory.StartOfString[4], RenderAttributes);
        }

        SdirNewlineThroughDisplay();

        if (!SdirRowDisplayed()) {
            YoriLibFreeStringContents(&NextSubDir);
            return FALSE;
        }

        if (!SdirDisplayCollection()) {
            YoriLibFreeStringContents(&NextSubDir);
            return FALSE;
        }
    }

    //
    //  Now traverse down the tree through all directories,
    //  optionally following links.
    //

    SdirDirCollectionCurrent = 0;
    SdirDirCollectionLongest = 0;
    SdirDirCollectionTotalNameLength = 0;

    if (ParentDirectory.LengthInChars == 0 ||
        ParentDirectory.StartOfString[ParentDirectory.LengthInChars - 1] == '\\') {

        szFormatStr = _T("%y*");
    } else {
        szFormatStr = _T("%y\\*");
    }

    if (YoriLibYPrintf(&NextSubDir, szFormatStr, &ParentDirectory) < 0 ||
        NextSubDir.LengthInChars == 0) {

        SdirWriteString(_T("Path exceeds allocated length\n"));
        YoriLibFreeStringContents(&NextSubDir);
        return FALSE;
    }

    hFind = FindFirstFile(NextSubDir.StartOfString, &FindData);

    if (hFind == NULL || hFind == INVALID_HANDLE_VALUE) {
        DWORD Err = GetLastError();
        Opts->ErrorsFound = TRUE;
        if (SdirIsReportableError(Err)) {
            if (!SdirDisplayYsError(Err, &NextSubDir)) {
                YoriLibFreeStringContents(&NextSubDir);
                return FALSE;
            }
        }
        if (!SdirContinuableError(Err)) {
            YoriLibFreeStringContents(&NextSubDir);
            return FALSE;
        } else {
            YoriLibFreeStringContents(&NextSubDir);
            return TRUE;
        }
    }

    do {
        if (FindData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY &&
            _tcscmp(FindData.cFileName, _T(".")) != 0 &&
            _tcscmp(FindData.cFileName, _T("..")) != 0) {

            if (Opts->TraverseLinks ||
                (FindData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0 ||
                (FindData.dwReserved0 != IO_REPARSE_TAG_MOUNT_POINT &&
                 FindData.dwReserved0 != IO_REPARSE_TAG_SYMLINK)) {

                if (ParentDirectory.LengthInChars == 0 ||
                    ParentDirectory.StartOfString[ParentDirectory.LengthInChars - 1] == '\\') {
                    szFormatStr = _T("%y%s\\%y");
                } else {
                    szFormatStr = _T("%y\\%s\\%y");
                }

                if (YoriLibYPrintf(&NextSubDir, szFormatStr, &ParentDirectory, FindData.cFileName, &SearchCriteria) < 0 ||
                    NextSubDir.LengthInChars == 0) {

                    SdirWriteString(_T("Path exceeds allocated length\n"));
                    YoriLibFreeStringContents(&NextSubDir);
                    return FALSE;
                }

                if (!SdirEnumerateAndDisplaySubtree(Depth + 1, &NextSubDir)) {
                    FindClose(hFind);
                    YoriLibFreeStringContents(&NextSubDir);
                    return FALSE;
                }
            }
        }
    } while (FindNextFile(hFind, &FindData) && !Opts->Cancelled);

    YoriLibFreeStringContents(&NextSubDir);

    FindClose(hFind);

    return TRUE;
}

/**
 Walk through each of the arguments to the program and enumerate each
 recursively.

 @param ArgC Number of arguments passed to the program.

 @param ArgV An array of each argument passed to the program.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SdirEnumerateAndDisplayRecursive (
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOLEAN EnumerateUserSpecified = FALSE;
    ULONG CurrentArg;
    YORI_STRING Arg;
    YORI_STRING FindStr;

    for (CurrentArg = 1; CurrentArg < ArgC; CurrentArg++) {
        if (!YoriLibIsCommandLineOption(&ArgV[CurrentArg], &Arg)) {

            YoriLibInitEmptyString(&FindStr);
            if (!YoriLibUserStringToSingleFilePath(&ArgV[CurrentArg], TRUE, &FindStr)) {
                return FALSE;
            }

            if (!SdirEnumerateAndDisplaySubtree(0, &FindStr)) {
                YoriLibFreeStringContents(&FindStr);
                return FALSE;
            }
            YoriLibFreeStringContents(&FindStr);
            EnumerateUserSpecified = TRUE;
        }
    }

    if (!EnumerateUserSpecified) {
        YoriLibConstantString(&Arg, _T("*"));
        YoriLibInitEmptyString(&FindStr);
        if (!YoriLibUserStringToSingleFilePath(&Arg, TRUE, &FindStr)) {
            return FALSE;
        }
        if (!SdirEnumerateAndDisplaySubtree(0, &FindStr)) {
            YoriLibFreeStringContents(&FindStr);
            return FALSE;
        }
        YoriLibFreeStringContents(&FindStr);
    }

    return TRUE;
}

#ifdef YORI_BUILTIN
/**
 The main entrypoint for the sdir builtin command.
 */
#define ENTRYPOINT YoriCmd_SDIR
#else
/**
 The main entrypoint for the sdir standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the sdir command.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    SdirAllocatedDirents = 1000;
    SdirDirCollection = NULL;
    SdirDirSorted = NULL;
    SdirDirCollectionCurrent = 0;
    SdirDirCollectionLongest = 0;
    SdirDirCollectionTotalNameLength = 0;
    SdirWriteStringLinesDisplayed = 0;

    if (!SdirInit(ArgC, ArgV)) {
        goto restore_and_exit;
    }

    if (Opts->Recursive) {
        if (!SdirEnumerateAndDisplayRecursive(ArgC, ArgV)) {
            goto restore_and_exit;
        }

        if (Opts->ErrorsFound) {
            SdirWriteStringWithAttribute(_T("Errors found during enumerate; results are incomplete\n"), Opts->FtError.HighlightColor);
        }
    } else {
        if (!SdirEnumerateAndDisplay(ArgC, ArgV)) {
            goto restore_and_exit;
        }
    }

    if (Opts->FtSummary.Flags & SDIR_FEATURE_DISPLAY) {

        SdirDisplaySummary(Opts->FtSummary.HighlightColor);
    }

restore_and_exit:

    if (Opts != NULL) {
        SdirSetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), Opts->PreviousAttributes);
    }
    SdirAppCleanup();

    return 0;
}

// vim:sw=4:ts=4:et:


<|start_filename|>copy/copy.c<|end_filename|>
/**
 * @file copy/copy.c
 *
 * Yori shell copy files
 *
 * Copyright (c) 2017-2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, COPYESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

#ifndef SE_CREATE_SYMBOLIC_LINK_NAME
/**
 If the compilation environment hasn't defined it, define the privilege
 for creating symbolic links.
 */
#define SE_CREATE_SYMBOLIC_LINK_NAME _T("SeCreateSymbolicLinkPrivilege")
#endif

#ifndef FILE_FLAG_OPEN_NO_RECALL
/**
 If the compilation environment hasn't defined it, define the flag for not
 recalling objects from slow storage.
 */
#define FILE_FLAG_OPEN_NO_RECALL 0x100000
#endif

/**
 Help text to display to the user.
 */
const
CHAR strCopyHelpText[] =
        "\n"
        "Copies one or more files.\n"
        "\n"
        "COPY [-license] [-b] [-c:algorithm] [-l] [-n|-nt|-p] [-s] [-t] [-v] [-x exclude]\n"
        "      <src>\n"
        "COPY [-license] [-b] [-c:algorithm] [-l] [-n|-nt|-p] [-s] [-t] [-v] [-x exclude]\n"
        "      <src> [<src> ...] <dest>\n"
        "\n"
        "   -b             Use basic search criteria for files only\n"
        "   -c             Compress targets with specified algorithm.  Options are:\n"
        "                    lzx, ntfs, xp4k, xp8k, xp16k\n"
        "   -l             Copy links as links rather than contents\n"
        "   -n             Copy new or files whose size have changed only\n"
        "   -nt            Copy new or files whose size or timestamps have changed only\n"
        "   -p             Preserve existing files, no overwriting\n"
        "   -s             Copy subdirectories as well as files\n"
        "   -t             Copy timestamps only, no data\n"
        "   -v             Verbose output\n"
        "   -x             Exclude files matching specified pattern\n";

/**
 Display usage text to the user.
 */
BOOL
CopyHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Copy %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strCopyHelpText);
    return TRUE;
}

/**
 A single item to exclude.  Note this can refer to multiple files.
 */
typedef struct _COPY_EXCLUDE_ITEM {

    /**
     List of items to exclude.
     */
    YORI_LIST_ENTRY ExcludeList;

    /**
     A string describing the object to exclude, which may include
     wildcards.
     */
    YORI_STRING ExcludeCriteria;
} COPY_EXCLUDE_ITEM, *PCOPY_EXCLUDE_ITEM;

/**
 A context passed between each source file match when copying multiple
 files.
 */
typedef struct _COPY_CONTEXT {
    /**
     Path to the destination for the copy operation.
     */
    YORI_STRING Dest;

    /**
     Files matching any of the exclude rules will not be copied.
     */
    YORI_LIST_ENTRY ExcludeList;

    /**
     State related to background compression of files after copy.
     */
    YORILIB_COMPRESS_CONTEXT CompressContext;

    /**
     The file system attributes of the destination.  Used to determine if
     the destination exists and is a directory.
     */
    DWORD DestAttributes;

    /**
     The number of files that have been previously copied to this
     destination.  This can be used to determine if we're about to copy
     a second object over the top of an earlier copied file.
     */
    DWORD FilesCopied;

    /**
     The number of files that have been enumerated while expanding a
     particular command argument.  If this is zero, the argument hasn't
     resolved to any existing files.
     */
    DWORD FilesFoundThisArg;

    /**
     If TRUE, targets should be compressed.
     */
    BOOLEAN CompressDest;

    /**
     If TRUE, links are copied as links rather than having their contents
     copied.
     */
    BOOLEAN CopyAsLinks;

    /**
     If TRUE, files are copied if they are not on the target, or if the size
     on the source is different to the target.  Depending on the value of
     CopyChangedTimestamps, a file may be copied if its size is identical
     on the source and target but the timestamp has changed.
     */
    BOOLEAN CopyNewOnly;

    /**
     If TRUE, files are copied if the timestamp has changed despite other
     attributes being the same.  If FALSE, the timestamp is ignored, and
     files will not be copied unless other attributes have changed.  This
     field is only meaningful if CopyNewOnly is TRUE.
     */
    BOOLEAN CopyChangedTimestamps;

    /**
     If TRUE, files are copied if they do not already exists.  Any existing
     file will be skipped.
     */
    BOOLEAN PreserveExisting;

    /**
     If TRUE, times from the source are explicitly copied to the target. If
     FALSE, this task is left to CopyFile's defaults.
     */
    BOOLEAN CopyTimestamps;

    /**
     If TRUE, data copies are skipped.  This is done when timestamps are
     being copied on existing files without moving any data.
     */
    BOOLEAN SkipDataCopy;

    /**
     If TRUE, the destination is a device rather than a file, and CopyFile
     should not be used since setting file metadata on the device is
     expected to fail.
     */
    BOOLEAN DestinationIsDevice;

    /**
     If TRUE, output is generated for each object copied.
     */
    BOOLEAN Verbose;
} COPY_CONTEXT, *PCOPY_CONTEXT;

/**
 Add a new exclude criteria to the list.

 @param CopyContext Pointer to the copy context to populate with a new
        exclude criteria.

 @param NewCriteria Pointer to the new criteria to add, which may include
        wildcards.
 
 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
CopyAddExclude(
    __in PCOPY_CONTEXT CopyContext,
    __in PYORI_STRING NewCriteria
    )
{
    PCOPY_EXCLUDE_ITEM ExcludeItem;
    ExcludeItem = YoriLibReferencedMalloc(sizeof(COPY_EXCLUDE_ITEM) + (NewCriteria->LengthInChars + 1) * sizeof(TCHAR));

    if (ExcludeItem == NULL) {
        return FALSE;
    }

    ZeroMemory(ExcludeItem, sizeof(COPY_EXCLUDE_ITEM));
    ExcludeItem->ExcludeCriteria.StartOfString = (LPTSTR)(ExcludeItem + 1);
    ExcludeItem->ExcludeCriteria.LengthInChars = NewCriteria->LengthInChars;
    ExcludeItem->ExcludeCriteria.LengthAllocated = NewCriteria->LengthInChars + 1;
    memcpy(ExcludeItem->ExcludeCriteria.StartOfString, NewCriteria->StartOfString, ExcludeItem->ExcludeCriteria.LengthInChars * sizeof(TCHAR));
    ExcludeItem->ExcludeCriteria.StartOfString[ExcludeItem->ExcludeCriteria.LengthInChars] = '\0';
    YoriLibAppendList(&CopyContext->ExcludeList, &ExcludeItem->ExcludeList);
    return TRUE;
}

/**
 Free all previously added exclude criteria.

 @param CopyContext Pointer to the copy context to free all exclude criteria
        from.
 */
VOID
CopyFreeExcludes(
    __in PCOPY_CONTEXT CopyContext
    )
{
    PCOPY_EXCLUDE_ITEM ExcludeItem;
    PYORI_LIST_ENTRY ListEntry;

    ListEntry = YoriLibGetNextListEntry(&CopyContext->ExcludeList, NULL);
    while (ListEntry != NULL) {
        ExcludeItem = CONTAINING_RECORD(ListEntry, COPY_EXCLUDE_ITEM, ExcludeList);
        YoriLibRemoveListItem(&ExcludeItem->ExcludeList);
        YoriLibDereference(ExcludeItem);
        ListEntry = YoriLibGetNextListEntry(&CopyContext->ExcludeList, NULL);
    }
}


/**
 Construct a full path to the destination from a CopyContext which specifies
 the destination location, and the relative path from the source.

 @param CopyContext Pointer to a copy context specifying the destination.

 @param RelativePathFromSource Pointer to the file name relative to the source
        root.

 @param FullDest On successful completion, updated to point to a fully
        qualified name to the destination.

 @return TRUE to indicate success, FALSE to indicate failure.  Note this
         function can display errors to the console.
 */
__success(return)
BOOL
CopyBuildDestinationPath(
    __in PCOPY_CONTEXT CopyContext,
    __in PYORI_STRING RelativePathFromSource,
    __inout PYORI_STRING FullDest
    )
{
    //
    //  If the target is a directory, construct a full path to the object
    //  within the target's directory tree.  Otherwise, the target is just
    //  a regular file with no path.
    //

    if (CopyContext->DestAttributes & FILE_ATTRIBUTE_DIRECTORY) {
        YORI_STRING DestWithFile;

        if (!YoriLibAllocateString(&DestWithFile, CopyContext->Dest.LengthInChars + 1 + RelativePathFromSource->LengthInChars + 1)) {
            return FALSE;
        }
        DestWithFile.LengthInChars = YoriLibSPrintf(DestWithFile.StartOfString, _T("%y\\%y"), &CopyContext->Dest, RelativePathFromSource);
        if (!YoriLibGetFullPathNameReturnAllocation(&DestWithFile, TRUE, FullDest, NULL)) {
            return FALSE;
        }
        YoriLibFreeStringContents(&DestWithFile);
    } else {
        if (!YoriLibGetFullPathNameReturnAllocation(&CopyContext->Dest, TRUE, FullDest, NULL)) {
            return FALSE;
        }
        if (CopyContext->FilesCopied > 0) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Attempting to copy multiple files over a single file (%s)\n"), FullDest->StartOfString);
            YoriLibFreeStringContents(FullDest);
            return FALSE;
        }
    }
    return TRUE;
}

/**
 Returns TRUE to indicate that an object should be excluded based on the
 exclude criteria, or FALSE if it should be included.

 @param CopyContext Pointer to the copy context to check the new object
        against.

 @param RelativeSourcePath Pointer to a string describing the file relative
        to the root of the source of the copy operation.

 @param SourceFindData Pointer to information about the source as returned
        from directory enumeration.  This can be NULL if the source was not
        found from directory enumeration.

 @return TRUE to exclude the file, FALSE to include it.
 */
BOOL
CopyShouldExclude(
    __in PCOPY_CONTEXT CopyContext,
    __in PYORI_STRING RelativeSourcePath,
    __in_opt PWIN32_FIND_DATA SourceFindData
    )
{
    PCOPY_EXCLUDE_ITEM ExcludeItem;
    PYORI_LIST_ENTRY ListEntry;

    ListEntry = YoriLibGetNextListEntry(&CopyContext->ExcludeList, NULL);
    while (ListEntry != NULL) {
        ExcludeItem = CONTAINING_RECORD(ListEntry, COPY_EXCLUDE_ITEM, ExcludeList);
        if (YoriLibDoesFileMatchExpression(RelativeSourcePath, &ExcludeItem->ExcludeCriteria)) {
            return TRUE;
        }
        ListEntry = YoriLibGetNextListEntry(&CopyContext->ExcludeList, ListEntry);
    }

    if (CopyContext->CopyNewOnly || CopyContext->PreserveExisting) {
        YORI_STRING FullDest;
        BY_HANDLE_FILE_INFORMATION DestFileInfo;
        LARGE_INTEGER DestWriteTime;
        LARGE_INTEGER SourceWriteTime;
        HANDLE DestFileHandle;

        YoriLibInitEmptyString(&FullDest);

        if (!CopyBuildDestinationPath(CopyContext, RelativeSourcePath, &FullDest)) {
            return FALSE;
        }

        DestFileHandle = CreateFile(FullDest.StartOfString,
                                    FILE_READ_ATTRIBUTES,
                                    FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
                                    NULL,
                                    OPEN_EXISTING,
                                    FILE_FLAG_OPEN_REPARSE_POINT|FILE_FLAG_OPEN_NO_RECALL|FILE_FLAG_BACKUP_SEMANTICS,
                                    NULL);

        YoriLibFreeStringContents(&FullDest);

        if (DestFileHandle == INVALID_HANDLE_VALUE) {
            return FALSE;
        }

        if (CopyContext->PreserveExisting) {
            CloseHandle(DestFileHandle);
            return TRUE;
        }

        if (!GetFileInformationByHandle(DestFileHandle, &DestFileInfo)) {
            CloseHandle(DestFileHandle);
            return FALSE;
        }

        if (SourceFindData == NULL) {
            CloseHandle(DestFileHandle);
            return TRUE;
        }

        if (DestFileInfo.nFileSizeHigh != SourceFindData->nFileSizeHigh ||
            DestFileInfo.nFileSizeLow != SourceFindData->nFileSizeLow) {

            CloseHandle(DestFileHandle);
            return FALSE;
        }

        if (CopyContext->CopyChangedTimestamps) {

            DestWriteTime.HighPart = DestFileInfo.ftLastWriteTime.dwHighDateTime;
            DestWriteTime.LowPart = DestFileInfo.ftLastWriteTime.dwLowDateTime;
            SourceWriteTime.HighPart = SourceFindData->ftLastWriteTime.dwHighDateTime;
            SourceWriteTime.LowPart = SourceFindData->ftLastWriteTime.dwLowDateTime;

            //
            //  Due to file system timing granularity, if the source was written
            //  to more than 5 seconds before or after the target, consider it
            //  a timestamp change.
            //

            if (SourceWriteTime.QuadPart < DestWriteTime.QuadPart - 10 * 1000 * 1000 * 5 ||
                SourceWriteTime.QuadPart > DestWriteTime.QuadPart + 10 * 1000 * 1000 * 5) {

                CloseHandle(DestFileHandle);
                return FALSE;
            }
        }

        CloseHandle(DestFileHandle);
        return TRUE;
    }
    return FALSE;
}

/**
 Copy a single file from the source to the target by preserving its link
 contents.

 @param SourceFileName A NULL terminated string specifying the source file.

 @param DestFileName A NULL terminated string specifying the destination file.

 @param IsDirectory TRUE if the object being copied is a directory, FALSE if
        it is not.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
CopyAsLink(
    __in LPTSTR SourceFileName,
    __in LPTSTR DestFileName,
    __in BOOL IsDirectory
    )
{
    PVOID ReparseData;
    HANDLE SourceFileHandle;
    HANDLE DestFileHandle;
    DWORD LastError;
    DWORD BytesReturned;
    LPTSTR ErrText;

    SourceFileHandle = CreateFile(SourceFileName,
                                  GENERIC_READ,
                                  FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
                                  NULL,
                                  OPEN_EXISTING,
                                  FILE_FLAG_OPEN_REPARSE_POINT|FILE_FLAG_OPEN_NO_RECALL|FILE_FLAG_BACKUP_SEMANTICS,
                                  NULL);

    if (SourceFileHandle == INVALID_HANDLE_VALUE) {
        LastError = GetLastError();
        ErrText = YoriLibGetWinErrorText(LastError);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Open of source failed: %s: %s"), SourceFileName, ErrText);
        YoriLibFreeWinErrorText(ErrText);
        return FALSE;
    }

    if (IsDirectory) {
        if (!CreateDirectory(DestFileName, NULL)) {
            LastError = GetLastError();
            if (LastError != ERROR_ALREADY_EXISTS) {
                ErrText = YoriLibGetWinErrorText(LastError);
                YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Create of destination failed: %s: %s"), DestFileName, ErrText);
                YoriLibFreeWinErrorText(ErrText);
                CloseHandle(SourceFileHandle);
                return FALSE;
            }
        }

        DestFileHandle = CreateFile(DestFileName,
                                    GENERIC_WRITE,
                                    FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
                                    NULL,
                                    OPEN_EXISTING,
                                    FILE_FLAG_OPEN_REPARSE_POINT|FILE_FLAG_OPEN_NO_RECALL|FILE_FLAG_BACKUP_SEMANTICS,
                                    NULL);

        if (DestFileHandle == INVALID_HANDLE_VALUE) {
            LastError = GetLastError();
            ErrText = YoriLibGetWinErrorText(LastError);
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Open of destination failed: %s: %s"), DestFileName, ErrText);
            YoriLibFreeWinErrorText(ErrText);
            CloseHandle(SourceFileHandle);
            RemoveDirectory(DestFileName);
            return FALSE;
        }
    } else {
        DestFileHandle = CreateFile(DestFileName,
                                    GENERIC_WRITE,
                                    FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
                                    NULL,
                                    CREATE_ALWAYS,
                                    FILE_FLAG_OPEN_REPARSE_POINT|FILE_FLAG_OPEN_NO_RECALL|FILE_FLAG_BACKUP_SEMANTICS,
                                    NULL);

        if (DestFileHandle == INVALID_HANDLE_VALUE) {
            LastError = GetLastError();
            ErrText = YoriLibGetWinErrorText(LastError);
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Open of destination failed: %s: %s"), DestFileName, ErrText);
            YoriLibFreeWinErrorText(ErrText);
            CloseHandle(SourceFileHandle);
            return FALSE;
        }
    }

    ReparseData = YoriLibMalloc(64 * 1024);
    if (ReparseData == NULL) {
        CloseHandle(DestFileHandle);
        CloseHandle(SourceFileHandle);
        if (IsDirectory) {
            RemoveDirectory(DestFileName);
        } else {
            DeleteFile(DestFileName);
        }
        return FALSE;
    }

    if (!DeviceIoControl(SourceFileHandle, FSCTL_GET_REPARSE_POINT, NULL, 0, ReparseData, 64 * 1024, &BytesReturned, NULL)) {
        LastError = GetLastError();
        ErrText = YoriLibGetWinErrorText(LastError);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Querying reparse data of source failed: %s: %s"), SourceFileName, ErrText);
        YoriLibFreeWinErrorText(ErrText);
        CloseHandle(SourceFileHandle);
        CloseHandle(DestFileHandle);
        YoriLibFree(ReparseData);
        if (IsDirectory) {
            RemoveDirectory(DestFileName);
        } else {
            DeleteFile(DestFileName);
        }
        return FALSE;
    }

    if (!DeviceIoControl(DestFileHandle, FSCTL_SET_REPARSE_POINT, ReparseData, BytesReturned, NULL, 0, &BytesReturned, NULL)) {
        LastError = GetLastError();
        ErrText = YoriLibGetWinErrorText(LastError);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Setting reparse data on dest failed: %s: %s"), DestFileName, ErrText);
        YoriLibFreeWinErrorText(ErrText);
        CloseHandle(SourceFileHandle);
        CloseHandle(DestFileHandle);
        YoriLibFree(ReparseData);
        if (IsDirectory) {
            RemoveDirectory(DestFileName);
        } else {
            DeleteFile(DestFileName);
        }
        return FALSE;
    }

    CloseHandle(SourceFileHandle);
    CloseHandle(DestFileHandle);
    YoriLibFree(ReparseData);
    return TRUE;
}

/**
 For objects that are not really files, copy can't use CopyFile, and instead
 falls back to this stupid thing of reading and writing.  Note this path
 should not be used for files since it makes no attempt to preserve any kind
 of file metadata, but for devices file metadata is meaningless anyway.

 @param SourceFile Pointer to the source file/device name.

 @param DestFile Pointer to the destination file/device name.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
CopyAsDumbDataMove(
    __in PYORI_STRING SourceFile,
    __in PYORI_STRING DestFile
    )
{
    PVOID Buffer;
    DWORD BytesCopied;
    DWORD BufferSize;
    HANDLE SourceHandle;
    HANDLE DestHandle;
    DWORD LastError;
    LPTSTR ErrText;

    SourceHandle = CreateFile(SourceFile->StartOfString,
                              GENERIC_READ,
                              FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
                              NULL,
                              OPEN_EXISTING,
                              FILE_FLAG_OPEN_NO_RECALL|FILE_FLAG_BACKUP_SEMANTICS,
                              NULL);

    if (SourceHandle == INVALID_HANDLE_VALUE) {
        LastError = GetLastError();
        ErrText = YoriLibGetWinErrorText(LastError);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Open of source failed: %y: %s"), SourceFile, ErrText);
        YoriLibFreeWinErrorText(ErrText);
        return FALSE;
    }

    DestHandle = CreateFile(DestFile->StartOfString,
                            GENERIC_WRITE,
                            FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
                            NULL,
                            CREATE_ALWAYS,
                            FILE_FLAG_BACKUP_SEMANTICS,
                            NULL);

    LastError = GetLastError();

    if (LastError == ERROR_INVALID_PARAMETER) {
        DestHandle = CreateFile(DestFile->StartOfString,
                                GENERIC_WRITE,
                                FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
                                NULL,
                                OPEN_EXISTING,
                                FILE_FLAG_BACKUP_SEMANTICS,
                                NULL);
    }

    if (DestHandle == INVALID_HANDLE_VALUE) {
        ErrText = YoriLibGetWinErrorText(LastError);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Open of destination failed: %y: %s"), DestFile, ErrText);
        YoriLibFreeWinErrorText(ErrText);
        CloseHandle(SourceHandle);
        return FALSE;
    }

    BufferSize = 64 * 1024;
    Buffer = YoriLibMalloc(BufferSize);
    if (Buffer == NULL) {
        CloseHandle(SourceHandle);
        CloseHandle(DestHandle);
        return FALSE;
    }

    while (ReadFile(SourceHandle, Buffer, BufferSize, &BytesCopied, NULL)) {
        if (BytesCopied == 0) {
            break;
        }

        if (!WriteFile(DestHandle, Buffer, BytesCopied, &BytesCopied, NULL)) {
            LastError = GetLastError();
            ErrText = YoriLibGetWinErrorText(LastError);
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Write to destination failed: %y: %s"), DestFile, ErrText);
            YoriLibFreeWinErrorText(ErrText);
            YoriLibFree(Buffer);
            CloseHandle(SourceHandle);
            CloseHandle(DestHandle);
            return FALSE;
        }
    }

    YoriLibFree(Buffer);
    CloseHandle(SourceHandle);
    CloseHandle(DestHandle);
    return TRUE;
}

/**
 Apply the timestamps from the source enumeration to the target file.  This
 can be done as a standalone operation or as part of updating files to
 newer contents, where it is important that the timestamps of the target are
 updated.

 @param SourceFindData Pointer to the enumeration from the source specifying
        file times to apply.

 @param DestFile Points to the fully qualified pathname to the target to
        apply timestamps to.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
CopyTimestamps(
    __in PWIN32_FIND_DATA SourceFindData,
    __in PYORI_STRING DestFile
    )
{
    HANDLE DestFileHandle;

    DestFileHandle = CreateFile(DestFile->StartOfString,
                                FILE_WRITE_ATTRIBUTES,
                                FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
                                NULL,
                                OPEN_EXISTING,
                                FILE_FLAG_OPEN_REPARSE_POINT|FILE_FLAG_OPEN_NO_RECALL|FILE_FLAG_BACKUP_SEMANTICS,
                                NULL);

    if (DestFileHandle == INVALID_HANDLE_VALUE) {
        return FALSE;
    }

    if (!SetFileTime(DestFileHandle, &SourceFindData->ftCreationTime, &SourceFindData->ftLastAccessTime, &SourceFindData->ftLastWriteTime)) {
        CloseHandle(DestFileHandle);
        return FALSE;
    }

    CloseHandle(DestFileHandle);
    return TRUE;
}

/**
 A callback that is invoked when a file is found that matches a search criteria
 specified in the set of strings to enumerate.

 @param FilePath Pointer to the file path that was found.

 @param FileInfo Information about the file.  This can be NULL if the file was
        not found from enumeration, since the file may not be a file system
        object (ie., it may be a device.)

 @param Depth Indicates the recursion depth.  Used by copy to check if it
        needs to create new directories in the destination path.

 @param Context Pointer to a context block specifying the destination of the
        copy, indicating parameters to the copy operation, and tracking how
        many objects have been copied.

 @return TRUE to continute enumerating, FALSE to abort.
 */
BOOL
CopyFileFoundCallback(
    __in PYORI_STRING FilePath,
    __in_opt PWIN32_FIND_DATA FileInfo,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    PCOPY_CONTEXT CopyContext = (PCOPY_CONTEXT)Context;
    YORI_STRING RelativePathFromSource;
    YORI_STRING FullDest;
    YORI_STRING HumanSourcePath;
    YORI_STRING HumanDestPath;
    PYORI_STRING SourceNameToDisplay;
    PYORI_STRING DestNameToDisplay;
    DWORD SlashesFound;
    DWORD Index;
    DWORD LastError;

    ASSERT(YoriLibIsStringNullTerminated(FilePath));

    YoriLibInitEmptyString(&FullDest);
    YoriLibInitEmptyString(&RelativePathFromSource);
    YoriLibInitEmptyString(&HumanSourcePath);
    YoriLibInitEmptyString(&HumanDestPath);
    SourceNameToDisplay = FilePath;

    SlashesFound = 0;
    for (Index = FilePath->LengthInChars; Index > 0; Index--) {
        if (FilePath->StartOfString[Index - 1] == '\\') {
            SlashesFound++;
            if (SlashesFound == Depth + 1) {
                break;
            }
        }
    }

    ASSERT(Index > 0);
    ASSERT(SlashesFound == Depth + 1);

    RelativePathFromSource.StartOfString = &FilePath->StartOfString[Index];
    RelativePathFromSource.LengthInChars = FilePath->LengthInChars - Index;

    //
    //  Check if the user wanted to exclude this file
    //

    if (CopyShouldExclude(CopyContext, &RelativePathFromSource, FileInfo)) {
        CopyContext->FilesFoundThisArg++;

        if (CopyContext->Verbose) {
            if (YoriLibUnescapePath(FilePath, &HumanSourcePath)) {
                SourceNameToDisplay = &HumanSourcePath;
            }
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Skipping %y\n"), SourceNameToDisplay);
            YoriLibFreeStringContents(&HumanSourcePath);
        }

        return TRUE;
    }

    if (!CopyBuildDestinationPath(CopyContext, &RelativePathFromSource, &FullDest)) {
        CopyContext->FilesFoundThisArg++;
        return FALSE;
    }

    DestNameToDisplay = &FullDest;

    if (CopyContext->Verbose) {
        if (YoriLibUnescapePath(FilePath, &HumanSourcePath)) {
            SourceNameToDisplay = &HumanSourcePath;
        }
        if (YoriLibUnescapePath(&FullDest, &HumanDestPath)) {
            DestNameToDisplay = &HumanDestPath;
        }
        YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Copying %y to %y\n"), SourceNameToDisplay, DestNameToDisplay);
    }


    if (!CopyContext->SkipDataCopy) {
        if (FileInfo != NULL &&
            FileInfo->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT &&
            CopyContext->CopyAsLinks &&
            (FileInfo->dwReserved0 == IO_REPARSE_TAG_MOUNT_POINT || FileInfo->dwReserved0 == IO_REPARSE_TAG_SYMLINK)) {

            CopyAsLink(FilePath->StartOfString, FullDest.StartOfString, (FileInfo->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY));

        } else if (FileInfo != NULL &&
                   FileInfo->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
            if (!CreateDirectory(FullDest.StartOfString, NULL)) {
                LastError = GetLastError();
                if (LastError != ERROR_ALREADY_EXISTS) {
                    LPTSTR ErrText = YoriLibGetWinErrorText(LastError);
                    YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("CreateDirectory failed: %s: %s"), FullDest.StartOfString, ErrText);
                    YoriLibFreeWinErrorText(ErrText);
                }
            }
        } else if (CopyContext->DestinationIsDevice || YoriLibIsFileNameDeviceName(FilePath)) {
            CopyAsDumbDataMove(FilePath, &FullDest);
        } else {
            LastError = YoriLibCopyFile(FilePath, &FullDest);
            if (LastError != ERROR_SUCCESS) {

                //
                //  If it failed with an error indicating CopyFile couldn't
                //  handle it, fall back to dumb data copy.  Note that this
                //  function will output its own errors, so from this point,
                //  error handling is over.
                //

                if (LastError == ERROR_INVALID_PARAMETER) {
                    CopyAsDumbDataMove(FilePath, &FullDest);
                } else {
                    LPTSTR ErrText = YoriLibGetWinErrorText(LastError);
                    if (SourceNameToDisplay != &HumanSourcePath) {
                        if (YoriLibUnescapePath(FilePath, &HumanSourcePath)) {
                            SourceNameToDisplay = &HumanSourcePath;
                        }
                    }
                    if (DestNameToDisplay != &HumanDestPath) {
                        if (YoriLibUnescapePath(&FullDest, &HumanDestPath)) {
                            DestNameToDisplay = &HumanDestPath;
                        }
                    }
                    YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("CopyFile failed: %y to %y: %s"), SourceNameToDisplay, DestNameToDisplay, ErrText);
                    YoriLibFreeWinErrorText(ErrText);
                }
            }

            if (CopyContext->CompressDest) {

                YoriLibCompressFileInBackground(&CopyContext->CompressContext, &FullDest);
            }
        }
    }

    if (CopyContext->CopyTimestamps && FileInfo != NULL) {
        CopyTimestamps(FileInfo, &FullDest);
    }

    CopyContext->FilesFoundThisArg++;
    CopyContext->FilesCopied++;
    YoriLibFreeStringContents(&FullDest);
    YoriLibFreeStringContents(&HumanSourcePath);
    YoriLibFreeStringContents(&HumanDestPath);
    return TRUE;
}

/**
 If the privilege for creating symbolic links is available to the process,
 enable it.

 @return TRUE to indicate the privilege was enabled, FALSE if it was not.
 */
BOOL
CopyEnableSymlinkPrivilege(VOID)
{
    TOKEN_PRIVILEGES TokenPrivileges;
    LUID SymlinkLuid;
    HANDLE TokenHandle;

    YoriLibLoadAdvApi32Functions();
    if (DllAdvApi32.pLookupPrivilegeValueW == NULL ||
        DllAdvApi32.pOpenProcessToken == NULL ||
        DllAdvApi32.pAdjustTokenPrivileges == NULL) {

        return FALSE;
    }

    if (!DllAdvApi32.pLookupPrivilegeValueW(NULL, SE_CREATE_SYMBOLIC_LINK_NAME, &SymlinkLuid)) {
        return FALSE;
    }

    if (!DllAdvApi32.pOpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &TokenHandle)) {
        return FALSE;
    }

    TokenPrivileges.PrivilegeCount = 1;
    TokenPrivileges.Privileges[0].Luid = SymlinkLuid;
    TokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
    if (!DllAdvApi32.pAdjustTokenPrivileges(TokenHandle, FALSE, &TokenPrivileges, 0, NULL, 0)) {
        CloseHandle(TokenHandle);
        return FALSE;
    }
    CloseHandle(TokenHandle);

    return TRUE;
}

/**
 Free the structures allocated within a copy context.  The structure itself
 is on the stack and is not freed.  This will wait for any outstanding
 compression work to complete.

 @param CopyContext Pointer to the context to free.
 */
VOID
CopyFreeCopyContext(
    __in PCOPY_CONTEXT CopyContext
    )
{
    YoriLibFreeCompressContext(&CopyContext->CompressContext);
    YoriLibFreeStringContents(&CopyContext->Dest);
    CopyFreeExcludes(CopyContext);
}

#ifdef YORI_BUILTIN
/**
 The main entrypoint for the copy builtin command.
 */
#define ENTRYPOINT YoriCmd_YCOPY
#else
/**
 The main entrypoint for the copy standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the copy cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the child process on success, or failure if the child
         could not be launched.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD FilesProcessed;
    DWORD FileCount;
    DWORD LastFileArg = 0;
    DWORD FirstFileArg = 0;
    DWORD MatchFlags;
    BOOL BasicEnumeration;
    BOOL Recursive;
    DWORD i;
    DWORD Result;
    COPY_CONTEXT CopyContext;
    YORILIB_COMPRESS_ALGORITHM CompressionAlgorithm;
    YORI_STRING Arg;

    FileCount = 0;
    Recursive = FALSE;
    BasicEnumeration = FALSE;
    ZeroMemory(&CopyContext, sizeof(CopyContext));
    CompressionAlgorithm.EntireAlgorithm = 0;

    YoriLibInitializeListHead(&CopyContext.ExcludeList);

    for (i = 1; i < ArgC; i++) {

        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));
        ArgumentUnderstood = FALSE;

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                CopyHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2017-2021"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("b")) == 0) {
                BasicEnumeration = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("c")) == 0) {
                CompressionAlgorithm.EntireAlgorithm = 0;
                CompressionAlgorithm.WofAlgorithm = FILE_PROVIDER_COMPRESSION_XPRESS16K;
                CopyContext.CompressDest = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("c:lzx")) == 0) {

                CompressionAlgorithm.EntireAlgorithm = 0;
                CompressionAlgorithm.WofAlgorithm = FILE_PROVIDER_COMPRESSION_LZX;
                CopyContext.CompressDest = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("c:ntfs")) == 0) {
                CompressionAlgorithm.EntireAlgorithm = 0;
                CompressionAlgorithm.NtfsAlgorithm = COMPRESSION_FORMAT_DEFAULT;
                CopyContext.CompressDest = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("c:xpress")) == 0 ||
                       YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("c:xp4k")) == 0) {
                CompressionAlgorithm.EntireAlgorithm = 0;
                CompressionAlgorithm.WofAlgorithm = FILE_PROVIDER_COMPRESSION_XPRESS4K;
                CopyContext.CompressDest = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("c:xp8k")) == 0) {
                CompressionAlgorithm.EntireAlgorithm = 0;
                CompressionAlgorithm.WofAlgorithm = FILE_PROVIDER_COMPRESSION_XPRESS8K;
                CopyContext.CompressDest = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("c:xp16k")) == 0) {
                CompressionAlgorithm.EntireAlgorithm = 0;
                CompressionAlgorithm.WofAlgorithm = FILE_PROVIDER_COMPRESSION_XPRESS16K;
                CopyContext.CompressDest = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("l")) == 0) {
                CopyContext.CopyAsLinks = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("n")) == 0) {
                CopyContext.PreserveExisting = FALSE;
                CopyContext.SkipDataCopy = FALSE;
                CopyContext.CopyNewOnly = TRUE;
                CopyContext.CopyChangedTimestamps = FALSE;
                CopyContext.CopyTimestamps = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("nt")) == 0) {
                CopyContext.PreserveExisting = FALSE;
                CopyContext.SkipDataCopy = FALSE;
                CopyContext.CopyNewOnly = TRUE;
                CopyContext.CopyChangedTimestamps = TRUE;
                CopyContext.CopyTimestamps = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("p")) == 0) {
                CopyContext.CopyNewOnly = FALSE;
                CopyContext.SkipDataCopy = FALSE;
                CopyContext.PreserveExisting = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("s")) == 0) {
                Recursive = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("t")) == 0) {
                CopyContext.CopyTimestamps = TRUE;
                CopyContext.SkipDataCopy = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("v")) == 0) {
                CopyContext.Verbose = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("x")) == 0) {
                if (i + 1 < ArgC) {
                    CopyAddExclude(&CopyContext, &ArgV[i + 1]);
                    ArgumentUnderstood = TRUE;
                    i++;
                }
            }
        } else {
            ArgumentUnderstood = TRUE;
            FileCount++;
            LastFileArg = i;
            if (FirstFileArg == 0) {
                FirstFileArg = i;
            }
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    if (FileCount == 0) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("copy: argument missing\n"));
        CopyFreeCopyContext(&CopyContext);
        return EXIT_FAILURE;
    } else if (FileCount == 1) {
        YoriLibConstantString(&CopyContext.Dest, _T("."));
    } else {
        if (!YoriLibUserStringToSingleFilePathOrDevice(&ArgV[LastFileArg], TRUE, &CopyContext.Dest)) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("copy: could not resolve %y\n"), &ArgV[LastFileArg]);
            CopyFreeCopyContext(&CopyContext);
            return EXIT_FAILURE;
        }
        if (YoriLibIsFileNameDeviceName(&ArgV[LastFileArg])) {
            CopyContext.DestinationIsDevice = TRUE;
        }
        FileCount--;
    }

    ASSERT(YoriLibIsStringNullTerminated(&CopyContext.Dest));

    if (CopyContext.CopyAsLinks) {
        if (!CopyEnableSymlinkPrivilege()) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("copy: warning: could not enable symlink privilege\n"));
        }
    }

    CopyContext.DestAttributes = GetFileAttributes(CopyContext.Dest.StartOfString);
    if (CopyContext.DestAttributes == 0xFFFFFFFF) {
        if (Recursive) {
            if (!CreateDirectory(CopyContext.Dest.StartOfString, NULL)) {
                DWORD LastError = GetLastError();
                LPTSTR ErrText = YoriLibGetWinErrorText(LastError);
                YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("CreateDirectory failed: %y: %s"), &CopyContext.Dest, ErrText);
                YoriLibFreeWinErrorText(ErrText);
                CopyFreeCopyContext(&CopyContext);
                return EXIT_FAILURE;
            }
            CopyContext.DestAttributes = GetFileAttributes(CopyContext.Dest.StartOfString);
        }

        if (CopyContext.DestAttributes == 0xFFFFFFFF) {
            CopyContext.DestAttributes = 0;
        }
    }

    if (CopyContext.CompressDest) {
        if (!YoriLibInitializeCompressContext(&CopyContext.CompressContext, CompressionAlgorithm)) {
            CopyFreeCopyContext(&CopyContext);
            return EXIT_FAILURE;
        }
        if (CopyContext.Verbose) {
            CopyContext.CompressContext.Verbose = TRUE;
        }
    }

#if YORI_BUILTIN
    YoriLibCancelEnable();
#endif

    CopyContext.FilesCopied = 0;
    FilesProcessed = 0;

    for (i = FirstFileArg; i <= LastFileArg; i++) {
        if (!YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            //
            //  Cmd's copy and xcopy both treat any specified directory as
            //  referring to the contents of the directory.  So if you use:
            //
            //  xcopy /s dir_a dir_b
            //
            //  The files in dir_a are copied to dir_b, and there is no dir_a
            //  in the result.
            //
            //  Here we deliberately deviate from that behavior, and copy
            //  the object known as dir_a under dir_b.  Referring to files
            //  requires specifying dir_a\* .  This seems more logical and
            //  flexible than cmd/xcopy, even though it is different.
            //
            //  The "copy directory contents" case is retained in
            //  non-recursive copies, because in that case presumably the
            //  intent was not to copy an empty directory.
            //

            MatchFlags = YORILIB_FILEENUM_RETURN_FILES;
            if (BasicEnumeration) {
                MatchFlags |= YORILIB_FILEENUM_BASIC_EXPANSION;
            }
            if (Recursive) {
                MatchFlags |= YORILIB_FILEENUM_RECURSE_AFTER_RETURN | YORILIB_FILEENUM_RETURN_DIRECTORIES;
                if (CopyContext.CopyAsLinks) {
                    MatchFlags |= YORILIB_FILEENUM_NO_LINK_TRAVERSE;
                }
            } else {
                MatchFlags |= YORILIB_FILEENUM_DIRECTORY_CONTENTS;
            }

            CopyContext.FilesFoundThisArg = 0;
            YoriLibForEachFile(&ArgV[i], MatchFlags, 0, CopyFileFoundCallback, NULL, &CopyContext);
            if (CopyContext.FilesFoundThisArg == 0) {
                YORI_STRING FullPath;
                if (YoriLibUserStringToSingleFilePathOrDevice(&ArgV[i], TRUE, &FullPath)) {
                    CopyFileFoundCallback(&FullPath, NULL, 0, &CopyContext);
                    YoriLibFreeStringContents(&FullPath);
                }
            }
            FilesProcessed++;
            if (FilesProcessed == FileCount) {
                break;
            }
        }
    }

    Result = EXIT_SUCCESS;

    if (CopyContext.FilesCopied == 0) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("copy: no matching files found\n"));
        Result = EXIT_FAILURE;
    }

    CopyFreeCopyContext(&CopyContext);

    return Result;
}

// vim:sw=4:ts=4:et:


<|start_filename|>env/env.c<|end_filename|>
/**
 * @file env/env.c
 *
 * Yori shell set environment state and execute command
 *
 * Copyright (c) 2019 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>
#if YORI_BUILTIN
#include <yoricall.h>
#endif

/**
 Help text to display to the user.
 */
const
CHAR strEnvHelpText[] =
        "\n"
        "Set environment state and execute a command.\n"
        "\n"
        "ENV [-license] [Var=Value] Command\n"
        "\n"
        "   --             Treat all further arguments as variables or commands\n";

/**
 Display usage text to the user.
 */
BOOL
EnvHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Env %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strEnvHelpText);
    return TRUE;
}

/**
 Process any arguments to the command that set or delete environment
 variables, and indicate on completion the first argument that indicates
 a command to execute.

 @param StartEnvArg The array index of the first argument to parse to see if
        it will change an environment variable.

 @param ArgC The total number of array elements.

 @param ArgV Pointer to the array of arguments.

 @param StartCmdArg On successful completion, updated to point to the first
        argument to scan for a command to execute.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
EnvModifyEnvironment(
    __in DWORD StartEnvArg,
    __in DWORD ArgC,
    __in YORI_STRING ArgV[],
    __out PDWORD StartCmdArg
    )
{
    YORI_STRING Variable;
    YORI_STRING Value;
    LPTSTR Equals;
    DWORD i;

    *StartCmdArg = 0;

    YoriLibInitEmptyString(&Variable);
    for (i = StartEnvArg; i < ArgC; i++) {
        Equals = YoriLibFindLeftMostCharacter(&ArgV[i], '=');
        if (Equals == NULL) {
            *StartCmdArg = i;
            break;
        }

        Variable.StartOfString = ArgV[i].StartOfString;
        Variable.LengthInChars = (DWORD)(Equals - ArgV[i].StartOfString);
        Variable.LengthAllocated = Variable.LengthInChars + 1;
        Variable.StartOfString[Variable.LengthInChars] = '\0';

        Value.StartOfString = &Equals[1];
        Value.LengthInChars = (DWORD)(ArgV[i].LengthInChars - Variable.LengthAllocated);
        Value.LengthAllocated = Value.LengthInChars + 1;

#ifdef YORI_BUILTIN
        if (Value.LengthInChars == 0) {
            YoriCallSetEnvironmentVariable(&Variable, NULL);
        } else {
            YoriCallSetEnvironmentVariable(&Variable, &Value);
        }
#else
        if (Value.LengthInChars == 0) {
            SetEnvironmentVariable(Variable.StartOfString, NULL);
        } else {
            SetEnvironmentVariable(Variable.StartOfString, Value.StartOfString);
        }
#endif
    }

    return TRUE;
}


#ifdef YORI_BUILTIN
/**
 The main entrypoint for the env builtin command.
 */
#define ENTRYPOINT YoriCmd_YENV
#else
/**
 The main entrypoint for the env standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the env cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the child process on success, or failure if the child
         could not be launched.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    YORI_STRING CmdLine;
    DWORD ExitCode;
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartEnvArg = 0;
    DWORD StartCmdArg = 0;
    YORI_STRING Arg;

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                EnvHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2019"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                ArgumentUnderstood = TRUE;
                StartEnvArg = i + 1;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartEnvArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    if (StartEnvArg == 0) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("env: argument not specified\n"));
        return EXIT_FAILURE;
    }

#ifdef YORI_BUILTIN
    {
        YORI_STRING SavedEnvironment;

        if (!YoriLibGetEnvironmentStrings(&SavedEnvironment)) {
            return EXIT_FAILURE;
        }

        EnvModifyEnvironment(StartEnvArg, ArgC, ArgV, &StartCmdArg);

        if (StartCmdArg == 0) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("env: argument not specified\n"));
            YoriLibBuiltinSetEnvironmentStrings(&SavedEnvironment);
            return EXIT_FAILURE;
        }
        if (!YoriLibBuildCmdlineFromArgcArgv(ArgC - StartCmdArg, &ArgV[StartCmdArg], TRUE, TRUE, &CmdLine)) {
            return EXIT_FAILURE;
        }

        YoriCallExecuteExpression(&CmdLine);
        YoriLibFreeStringContents(&CmdLine);
        YoriLibBuiltinSetEnvironmentStrings(&SavedEnvironment);

        ExitCode = YoriCallGetErrorLevel();
    }
#else
    {
        YORI_STRING Executable;
        PYORI_STRING ChildArgs;
        PROCESS_INFORMATION ProcessInfo;
        STARTUPINFO StartupInfo;

        EnvModifyEnvironment(StartEnvArg, ArgC, ArgV, &StartCmdArg);

        if (StartCmdArg == 0) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("env: argument not specified\n"));
            return EXIT_FAILURE;
        }

        ChildArgs = YoriLibMalloc((ArgC - StartCmdArg) * sizeof(YORI_STRING));
        if (ChildArgs == NULL) {
            return EXIT_FAILURE;
        }

        YoriLibInitEmptyString(&Executable);
        if (!YoriLibLocateExecutableInPath(&ArgV[StartCmdArg], NULL, NULL, &Executable) ||
            Executable.LengthInChars == 0) {

            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("env: unable to find executable\n"));
            YoriLibFree(ChildArgs);
            YoriLibFreeStringContents(&Executable);
            return EXIT_FAILURE;
        }


        memcpy(&ChildArgs[0], &Executable, sizeof(YORI_STRING));
        if (StartCmdArg + 1 < ArgC) {
            memcpy(&ChildArgs[1], &ArgV[StartCmdArg + 1], (ArgC - StartCmdArg - 1) * sizeof(YORI_STRING));
        }

        if (!YoriLibBuildCmdlineFromArgcArgv(ArgC - StartCmdArg, ChildArgs, TRUE, TRUE, &CmdLine)) {
            YoriLibFree(ChildArgs);
            YoriLibFreeStringContents(&Executable);
            return EXIT_FAILURE;
        }

        ASSERT(YoriLibIsStringNullTerminated(&CmdLine));

        memset(&StartupInfo, 0, sizeof(StartupInfo));
        StartupInfo.cb = sizeof(StartupInfo);

        if (!CreateProcess(NULL, CmdLine.StartOfString, NULL, NULL, TRUE, CREATE_DEFAULT_ERROR_MODE, NULL, NULL, &StartupInfo, &ProcessInfo)) {
            DWORD LastError = GetLastError();
            LPTSTR ErrText = YoriLibGetWinErrorText(LastError);
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("env: execution failed: %s"), ErrText);
            YoriLibFreeWinErrorText(ErrText);
            YoriLibFree(ChildArgs);
            YoriLibFreeStringContents(&CmdLine);
            YoriLibFreeStringContents(&Executable);
            return EXIT_FAILURE;
        }

        WaitForSingleObject(ProcessInfo.hProcess, INFINITE);
        GetExitCodeProcess(ProcessInfo.hProcess, &ExitCode);
        CloseHandle(ProcessInfo.hProcess);
        CloseHandle(ProcessInfo.hThread);
        YoriLibFreeStringContents(&Executable);
        YoriLibFreeStringContents(&CmdLine);
        YoriLibFree(ChildArgs);
    }
#endif

    return ExitCode;
}

// vim:sw=4:ts=4:et:


<|start_filename|>builtins/pushd.c<|end_filename|>
/**
 * @file builtins/pushd.c
 *
 * Yori shell push and pop current directories
 *
 * Copyright (c) 2018-2019 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>
#include <yoricall.h>

/**
 Help text to display to the user.
 */
const
CHAR strPushdHelpText[] =
        "\n"
        "Push the current directory onto a stack and change to a new directory.\n"
        "If the new directory is unspecified, exchange the current directory\n"
        "and the top of the stack.\n"
        "\n"
        "PUSHD [-license] -c|-l|[<directory>]\n"
        "\n"
        "   -c             Display the number of directories on the pushd stack\n"
        "   -l             List outstanding directories on the pushd stack\n";

/**
 Display usage text to the user.
 */
BOOL
PushdHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("PushD %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strPushdHelpText);
    return TRUE;
}

/**
 Help text to display to the user.
 */
const
CHAR strPopdHelpText[] =
        "\n"
        "Pop a previous current directory from the stack.\n"
        "\n"
        "POPD [-license] [-c|-l]\n"
        "\n"
        "   -c             Display the number of directories on the pushd stack\n"
        "   -l             List outstanding directories on the pushd stack\n";

/**
 Display usage text to the user.
 */
BOOL
PopdHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("PopD %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strPopdHelpText);
    return TRUE;
}

/**
 Information describing saved state at the time of a pushd call.
 */
typedef struct _PUSHD_STACK {

    /**
     Pointers to the next most recent and previous in time pushd calls.
     */
    YORI_LIST_ENTRY StackLinks;

    /**
     A string containing the current directory at the time pushd was
     executed.
     */
    YORI_STRING PreviousDirectory;
} PUSHD_STACK, *PPUSHD_STACK;

/**
 A linked list of entries describing when pushd was executed.  The tail
 of this list corresponds to the most recent pushd call.
 */
YORI_LIST_ENTRY PushdStack;

/**
 Notification that the module is being unloaded or the shell is exiting,
 used to indicate any pending stack should be cleaned up.
 */
VOID
YORI_BUILTIN_FN
PushdNotifyUnload(VOID)
{
    PYORI_LIST_ENTRY ListEntry;
    PPUSHD_STACK StackLocation;

    if (PushdStack.Next == NULL) {
        return;
    }

    ListEntry = YoriLibGetPreviousListEntry(&PushdStack, NULL);
    while(ListEntry != NULL) {
        StackLocation = CONTAINING_RECORD(ListEntry, PUSHD_STACK, StackLinks);
        ListEntry = YoriLibGetPreviousListEntry(&PushdStack, ListEntry);
        YoriLibRemoveListItem(&StackLocation->StackLinks);
        YoriCallDecrementPromptRecursionDepth();
        YoriLibFree(StackLocation);
    }
}

/**
 Display any entries on the current pushd stack.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
PushdDisplayCurrentStack(VOID)
{
    PYORI_LIST_ENTRY ListEntry;
    PPUSHD_STACK StackLocation;

    if (PushdStack.Next == NULL) {
        return TRUE;
    }

    ListEntry = YoriLibGetPreviousListEntry(&PushdStack, NULL);
    while(ListEntry != NULL) {
        StackLocation = CONTAINING_RECORD(ListEntry, PUSHD_STACK, StackLinks);
        ListEntry = YoriLibGetPreviousListEntry(&PushdStack, ListEntry);
        YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%y\n"), &StackLocation->PreviousDirectory);
    }
    return TRUE;
}

/**
 Display the number of entries on the current pushd stack.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
PushdDisplayCurrentStackCount(VOID)
{
    PYORI_LIST_ENTRY ListEntry;
    PPUSHD_STACK StackLocation;
    DWORD Count = 0;

    if (PushdStack.Next != NULL) {
        ListEntry = YoriLibGetPreviousListEntry(&PushdStack, NULL);
        while(ListEntry != NULL) {
            Count++;
            StackLocation = CONTAINING_RECORD(ListEntry, PUSHD_STACK, StackLinks);
            ListEntry = YoriLibGetPreviousListEntry(&PushdStack, ListEntry);
        }
    }
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%i\n"), Count);
    return TRUE;
}

/**
 The main entrypoint for the popd command.

 @param ArgC The number of arguments.

 @param ArgV The argument array.

 @return ExitCode, zero for success, nonzero for failure.
 */
DWORD
YORI_BUILTIN_FN
YoriCmd_POPD(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    DWORD i;
    BOOL ArgumentUnderstood;
    BOOL ListStack = FALSE;
    BOOL CountStack = FALSE;
    PYORI_LIST_ENTRY ListEntry;
    PPUSHD_STACK StackLocation;
    YORI_STRING Arg;

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                PopdHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2018-2019"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("c")) == 0) {
                CountStack = TRUE;
                ListStack = FALSE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("l")) == 0) {
                CountStack = FALSE;
                ListStack = TRUE;
                ArgumentUnderstood = TRUE;
            }
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    if (CountStack) {
        if (PushdDisplayCurrentStackCount()) {
            return EXIT_SUCCESS;
        }
        return EXIT_FAILURE;
    } else if (ListStack) {
        if (PushdDisplayCurrentStack()) {
            return EXIT_SUCCESS;
        }
        return EXIT_FAILURE;
    }

    if (PushdStack.Next == NULL) {
        return EXIT_FAILURE;
    }

    ListEntry = YoriLibGetPreviousListEntry(&PushdStack, NULL);
    if (ListEntry == NULL) {
        return EXIT_FAILURE;
    }

    StackLocation = CONTAINING_RECORD(ListEntry, PUSHD_STACK, StackLinks);
    YoriLibRemoveListItem(&StackLocation->StackLinks);
    YoriCallDecrementPromptRecursionDepth();

    if (PushdStack.Next == &PushdStack) {
        YORI_STRING PopdCmd;
        YoriLibConstantString(&PopdCmd, _T("POPD"));
        YoriCallBuiltinUnregister(&PopdCmd, YoriCmd_POPD);
    }

    if (!YoriCallSetCurrentDirectory(&StackLocation->PreviousDirectory)) {
        YoriLibFree(StackLocation);
        return EXIT_FAILURE;
    }

    YoriLibFree(StackLocation);
    return EXIT_SUCCESS;
}


/**
 Push a directory onto the stack and switch to a new one.

 @param ArgC The number of arguments.

 @param ArgV The argument array.

 @return ExitCode, zero for success, nonzero for failure.
 */
DWORD
YORI_BUILTIN_FN
YoriCmd_PUSHD(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    PPUSHD_STACK NewStackEntry;
    DWORD CurrentDirectoryLength;
    YORI_STRING ChdirCmd;
    YORI_STRING Arg;
    BOOL ListStack = FALSE;
    BOOL CountStack = FALSE;
    PPUSHD_STACK StackLocation;
    PYORI_LIST_ENTRY ListEntry;

    YoriLibLoadNtDllFunctions();
    YoriLibLoadKernel32Functions();

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                PushdHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2018-2019"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("c")) == 0) {
                CountStack = TRUE;
                ListStack = FALSE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("l")) == 0) {
                ListStack = TRUE;
                CountStack = FALSE;
                ArgumentUnderstood = TRUE;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    if (CountStack) {
        if (PushdDisplayCurrentStackCount()) {
            return EXIT_SUCCESS;
        }
        return EXIT_FAILURE;
    } else if (ListStack) {
        if (PushdDisplayCurrentStack()) {
            return EXIT_SUCCESS;
        }
        return EXIT_FAILURE;
    }

    if (StartArg == 0 && YoriLibIsListEmpty(&PushdStack)) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("pushd: no other directory\n"));
        return EXIT_FAILURE;
    }

    CurrentDirectoryLength = GetCurrentDirectory(0, NULL);

    NewStackEntry = YoriLibMalloc(sizeof(PUSHD_STACK) + CurrentDirectoryLength * sizeof(TCHAR));
    if (NewStackEntry == NULL) {
        return EXIT_FAILURE;
    }

    YoriLibInitEmptyString(&NewStackEntry->PreviousDirectory);
    NewStackEntry->PreviousDirectory.StartOfString = (LPTSTR)(NewStackEntry + 1);
    NewStackEntry->PreviousDirectory.LengthAllocated = CurrentDirectoryLength;
    NewStackEntry->PreviousDirectory.LengthInChars = GetCurrentDirectory(CurrentDirectoryLength, NewStackEntry->PreviousDirectory.StartOfString);

    if (StartArg == 0) {

        //
        //  Exchange the current directory and the current top of the stack.
        //

        ListEntry = YoriLibGetPreviousListEntry(&PushdStack, NULL);
        StackLocation = CONTAINING_RECORD(ListEntry, PUSHD_STACK, StackLinks);
        if (!YoriCallSetCurrentDirectory(&StackLocation->PreviousDirectory)) {
            YoriLibFree(NewStackEntry);
            return EXIT_FAILURE;
        }

        YoriLibRemoveListItem(&StackLocation->StackLinks);
        YoriLibFree(StackLocation);
        YoriLibAppendList(&PushdStack, &NewStackEntry->StackLinks);

    } else {

        YORI_STRING ChdirArgV[2];

        //
        //  Invoke chdir to actually change directory.  This provides consistent
        //  path parsing and will handle things like drive switching consistently.
        //

        YoriLibConstantString(&ChdirArgV[0], _T("CHDIR"));
        YoriLibInitEmptyString(&ChdirArgV[1]);
        ChdirArgV[1].StartOfString = ArgV[StartArg].StartOfString;
        ChdirArgV[1].LengthInChars = ArgV[StartArg].LengthInChars;

        if (!YoriLibBuildCmdlineFromArgcArgv(2, ChdirArgV, TRUE, TRUE, &ChdirCmd)) {
            YoriLibFree(NewStackEntry);
            return EXIT_FAILURE;
        }

        YoriCallExecuteExpression(&ChdirCmd);
        YoriLibFreeStringContents(&ChdirCmd);

        if (PushdStack.Next == NULL) {
            YoriLibInitializeListHead(&PushdStack);
        }
        if (PushdStack.Next == &PushdStack) {
            YORI_STRING PopdCmd;
            YoriLibConstantString(&PopdCmd, _T("POPD"));
            if (!YoriCallBuiltinRegister(&PopdCmd, YoriCmd_POPD)) {
                YoriLibFree(NewStackEntry);
                return EXIT_FAILURE;
            }

            YoriCallSetUnloadRoutine(PushdNotifyUnload);
        }

        YoriLibAppendList(&PushdStack, &NewStackEntry->StackLinks);
        YoriCallIncrementPromptRecursionDepth();
    }

    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>date/date.c<|end_filename|>
/**
 * @file date/date.c
 *
 * Yori shell display command line output
 *
 * Copyright (c) 2017-2019 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 Help text to display to the user.
 */
const
CHAR strDateHelpText[] =
        "\n"
        "Outputs the system date and time in a specified format.\n"
        "\n"
        "DATE [-license] [-t] [-u] [<fmt>]\n"
        "\n"
        "   -t             Include time in output when format not specified\n"
        "   -u             Display UTC rather than local time\n"
        "\n"
        "Format specifiers are:\n"
        "   $COUNT_MS$     The number of milliseconds since epoch with leading zero\n"
        "   $count_ms$     The number of milliseconds since epoch without leading zero\n"
        "   $DAY$          The current numeric day of the month with leading zero\n"
        "   $day$          The current numeric day of the month without leading zero\n"
        "   $HOUR$         The current hour in 24 hour format with leading zero\n"
        "   $hour$         The current hour in 24 hour format without leading zero\n"
        "   $MIN$          The current minute with leading zero\n"
        "   $min$          The current minute without leading zero\n"
        "   $MON$          The current numeric month with leading zero\n"
        "   $mon$          The current numeric month without leading zero\n"
        "   $MS$           The current number of milliseconds with leading zero\n"
        "   $ms$           The current number of milliseconds without leading zero\n"
        "   $SEC$          The current second with leading zero\n"
        "   $sec$          The current second without leading zero\n"
        "   $TICK$         The number of milliseconds since boot with leading zero\n"
        "   $tick$         The number of milliseconds since boot without leading zero\n"
        "   $YEAR$         The current year as four digits\n"
        "   $year$         The current year as two digits\n";

/**
 Display usage text to the user.
 */
BOOL
DateHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Date %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strDateHelpText);
    return TRUE;
}

/**
 Context structure to provide information needed to expand format variables.
 */
typedef struct _DATE_CONTEXT {

    /**
     The current system time.
     */
    SYSTEMTIME Time;

    /**
     The current tick count.
     */
    LARGE_INTEGER Tick;

} DATE_CONTEXT, *PDATE_CONTEXT;

/**
 A callback function to expand any known variables found when parsing the
 format string.

 @param OutputBuffer A pointer to the output buffer to populate with data
        if a known variable is found.

 @param VariableName The variable name to expand.

 @param Context Pointer to a SYSTEMTIME structure containing the data to
        populate.
 
 @return The number of characters successfully populated, or the number of
         characters required in order to successfully populate, or zero
         on error.
 */
DWORD
DateExpandVariables(
    __inout PYORI_STRING OutputBuffer,
    __in PYORI_STRING VariableName,
    __in PVOID Context
    )
{
    DWORD CharsNeeded;
    PDATE_CONTEXT DateContext = (PDATE_CONTEXT)Context;
    FILETIME Clock;
    LARGE_INTEGER liClock;

    liClock.QuadPart = 0;

    if (YoriLibCompareStringWithLiteral(VariableName, _T("YEAR")) == 0) {
        CharsNeeded = 4;
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("year")) == 0) {
        CharsNeeded = 2;
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("MON")) == 0) {
        CharsNeeded = 2;
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("mon")) == 0) {
        CharsNeeded = 2;
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("DAY")) == 0) {
        CharsNeeded = 2;
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("day")) == 0) {
        CharsNeeded = 2;
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("HOUR")) == 0) {
        CharsNeeded = 2;
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("hour")) == 0) {
        CharsNeeded = 2;
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("MIN")) == 0) {
        CharsNeeded = 2;
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("min")) == 0) {
        CharsNeeded = 2;
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("SEC")) == 0) {
        CharsNeeded = 2;
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("sec")) == 0) {
        CharsNeeded = 2;
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("MS")) == 0) {
        CharsNeeded = 4;
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("ms")) == 0) {
        CharsNeeded = 4;
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("COUNT_MS")) == 0) {
        if (!SystemTimeToFileTime(&DateContext->Time, &Clock)) {
            return 0;
        }
        liClock.HighPart = Clock.dwHighDateTime;
        liClock.LowPart = Clock.dwLowDateTime;
        liClock.QuadPart = liClock.QuadPart / 10000;
        CharsNeeded = YoriLibSPrintfSize(_T("%016lli"), liClock.QuadPart);
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("count_ms")) == 0) {
        if (!SystemTimeToFileTime(&DateContext->Time, &Clock)) {
            return 0;
        }
        liClock.HighPart = Clock.dwHighDateTime;
        liClock.LowPart = Clock.dwLowDateTime;
        liClock.QuadPart = liClock.QuadPart / 10000;
        CharsNeeded = YoriLibSPrintfSize(_T("%lli"), liClock.QuadPart);
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("TICK")) == 0) {
        CharsNeeded = YoriLibSPrintfSize(_T("%016lli"), DateContext->Tick.QuadPart);
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("tick")) == 0) {
        CharsNeeded = YoriLibSPrintfSize(_T("%lli"), DateContext->Tick.QuadPart);
    } else {
        return 0;
    }

    if (OutputBuffer->LengthAllocated <= CharsNeeded) {
        return CharsNeeded;
    }

    if (YoriLibCompareStringWithLiteral(VariableName, _T("YEAR")) == 0) {
        CharsNeeded = YoriLibSPrintf(OutputBuffer->StartOfString, _T("%04i"), DateContext->Time.wYear);
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("year")) == 0) {
        CharsNeeded = YoriLibSPrintf(OutputBuffer->StartOfString, _T("%02i"), DateContext->Time.wYear % 100);
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("MON")) == 0) {
        CharsNeeded = YoriLibSPrintf(OutputBuffer->StartOfString, _T("%02i"), DateContext->Time.wMonth);
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("mon")) == 0) {
        if (DateContext->Time.wMonth < 100) {
            CharsNeeded = YoriLibSPrintf(OutputBuffer->StartOfString, _T("%i"), DateContext->Time.wMonth);
        } else {
            CharsNeeded = 0;
        }
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("DAY")) == 0) {
        CharsNeeded = YoriLibSPrintf(OutputBuffer->StartOfString, _T("%02i"), DateContext->Time.wDay);
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("day")) == 0) {
        if (DateContext->Time.wDay < 100) {
            CharsNeeded = YoriLibSPrintf(OutputBuffer->StartOfString, _T("%i"), DateContext->Time.wDay);
        } else {
            CharsNeeded = 0;
        }
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("HOUR")) == 0) {
        CharsNeeded = YoriLibSPrintf(OutputBuffer->StartOfString, _T("%02i"), DateContext->Time.wHour);
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("hour")) == 0) {
        if (DateContext->Time.wHour < 100) {
            CharsNeeded = YoriLibSPrintf(OutputBuffer->StartOfString, _T("%i"), DateContext->Time.wHour);
        } else {
            CharsNeeded = 0;
        }
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("MIN")) == 0) {
        CharsNeeded = YoriLibSPrintf(OutputBuffer->StartOfString, _T("%02i"), DateContext->Time.wMinute);
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("min")) == 0) {
        if (DateContext->Time.wMinute < 100) {
            CharsNeeded = YoriLibSPrintf(OutputBuffer->StartOfString, _T("%i"), DateContext->Time.wMinute);
        } else {
            CharsNeeded = 0;
        }
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("SEC")) == 0) {
        CharsNeeded = YoriLibSPrintf(OutputBuffer->StartOfString, _T("%02i"), DateContext->Time.wSecond);
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("sec")) == 0) {
        if (DateContext->Time.wSecond < 100) {
            CharsNeeded = YoriLibSPrintf(OutputBuffer->StartOfString, _T("%i"), DateContext->Time.wSecond);
        } else {
            CharsNeeded = 0;
        }
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("MS")) == 0) {
        CharsNeeded = YoriLibSPrintf(OutputBuffer->StartOfString, _T("%04i"), DateContext->Time.wMilliseconds);
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("ms")) == 0) {
        if (DateContext->Time.wMilliseconds < 10000) {
            CharsNeeded = YoriLibSPrintf(OutputBuffer->StartOfString, _T("%i"), DateContext->Time.wMilliseconds);
        } else {
            CharsNeeded = 0;
        }
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("COUNT_MS")) == 0) {
        CharsNeeded = YoriLibSPrintf(OutputBuffer->StartOfString, _T("%016lli"), liClock.QuadPart);
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("count_ms")) == 0) {
        CharsNeeded = YoriLibSPrintf(OutputBuffer->StartOfString, _T("%lli"), liClock.QuadPart);
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("TICK")) == 0) {
        CharsNeeded = YoriLibSPrintf(OutputBuffer->StartOfString, _T("%016lli"), DateContext->Tick.QuadPart);
    } else if (YoriLibCompareStringWithLiteral(VariableName, _T("tick")) == 0) {
        CharsNeeded = YoriLibSPrintf(OutputBuffer->StartOfString, _T("%lli"), DateContext->Tick.QuadPart);
    }

    return CharsNeeded;
}

#ifdef YORI_BUILTIN
/**
 The main entrypoint for the date builtin command.
 */
#define ENTRYPOINT YoriCmd_YDATE
#else
/**
 The main entrypoint for the date standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the date cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the child process on success, or failure if the child
         could not be launched.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    BOOL UseUtc;
    BOOL DisplayTime;
    YORI_STRING DisplayString;
    LPTSTR DefaultDateFormatString = _T("$YEAR$$MON$$DAY$");
    LPTSTR DefaultTimeFormatString = _T("$YEAR$/$MON$/$DAY$ $HOUR$:$MIN$:$SEC$");
    YORI_STRING AllocatedFormatString;
    DWORD StartArg;
    DWORD i;
    YORI_STRING Arg;
    DATE_CONTEXT DateContext;

    StartArg = 0;
    UseUtc = FALSE;
    DisplayTime = FALSE;
    YoriLibInitEmptyString(&AllocatedFormatString);

    for (i = 1; i < ArgC; i++) {

        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        ArgumentUnderstood = FALSE;

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                DateHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2017-2019"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("t")) == 0) {
                DisplayTime = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("u")) == 0) {
                UseUtc = TRUE;
                ArgumentUnderstood = TRUE;
            }
        } else {
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    if (StartArg > 0) {
        if (!YoriLibBuildCmdlineFromArgcArgv(ArgC - StartArg, &ArgV[StartArg], TRUE, FALSE, &AllocatedFormatString)) {
            return EXIT_FAILURE;
        }
    } else {
        if (DisplayTime) {
            YoriLibConstantString(&AllocatedFormatString, DefaultTimeFormatString);
        } else {
            YoriLibConstantString(&AllocatedFormatString, DefaultDateFormatString);
        }
    }

    if (UseUtc) {
        GetSystemTime(&DateContext.Time);
    } else {
        GetLocalTime(&DateContext.Time);
    }

    if (DllKernel32.pGetTickCount64 != NULL) {
        DateContext.Tick.QuadPart = DllKernel32.pGetTickCount64();
    } else {
        DateContext.Tick.HighPart = 0;
        //
        //  Warning about using a deprecated function and how we should use
        //  GetTickCount64 instead.  The analyzer isn't smart enough to notice
        //  that when it's available, that's what we do.
        //
#if defined(_MSC_VER) && (_MSC_VER >= 1700)
#pragma warning(suppress: 28159)
#endif
        DateContext.Tick.LowPart = GetTickCount();
    }

    YoriLibInitEmptyString(&DisplayString);
    YoriLibExpandCommandVariables(&AllocatedFormatString, '$', FALSE, DateExpandVariables, &DateContext, &DisplayString);
    if (DisplayString.StartOfString != NULL) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%y"), &DisplayString);
        YoriLibFreeStringContents(&DisplayString);
    }
    YoriLibFreeStringContents(&AllocatedFormatString);

    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>pkglib/util.c<|end_filename|>
/**
 * @file pkglib/util.c
 *
 * Yori package manager helper functions
 *
 * Copyright (c) 2018-2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>
#include "yoripkgp.h"

/**
 Return a fully qualified path to the currently running program.

 @param ExecutableFile On successful completion, populated with the program
        file that is currently running.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOL
YoriPkgGetExecutableFile(
    __out PYORI_STRING ExecutableFile
    )
{
    YORI_STRING ModuleName;

    if (!YoriLibAllocateString(&ModuleName, 32768)) {
        return FALSE;
    }

    ModuleName.LengthInChars = GetModuleFileName(NULL, ModuleName.StartOfString, ModuleName.LengthAllocated);
    if (ModuleName.LengthInChars == 0) {
        YoriLibFreeStringContents(&ModuleName);
        return FALSE;
    }

    memcpy(ExecutableFile, &ModuleName, sizeof(YORI_STRING));
    return TRUE;
}


/**
 Return a fully qualified path to the directory containing the program.

 @param AppDirectory On successful completion, populated with the directory
        containing the application.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOL
YoriPkgGetApplicationDirectory(
    __out PYORI_STRING AppDirectory
    )
{
    YORI_STRING ModuleName;
    LPTSTR Ptr;

    if (!YoriPkgGetExecutableFile(&ModuleName)) {
        return FALSE;
    }

    Ptr = YoriLibFindRightMostCharacter(&ModuleName, '\\');
    if (Ptr == NULL) {
        YoriLibFreeStringContents(&ModuleName);
        return FALSE;
    }

    *Ptr = '\0';

    ModuleName.LengthInChars = (DWORD)(Ptr - ModuleName.StartOfString);
    memcpy(AppDirectory, &ModuleName, sizeof(YORI_STRING));
    return TRUE;
}

/**
 Return a fully qualified path to the global package INI file.

 @param InstallDirectory Optionally specifies an install directory.  If not
        specified, the directory of the currently running application is
        used.  The INI file is always in this directory if it is specified.

 @param IniFileName On successful completion, populated with a path to the
        package INI file.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOL
YoriPkgGetPackageIniFile(
    __in_opt PCYORI_STRING InstallDirectory,
    __out PYORI_STRING IniFileName
    )
{
    YORI_STRING AppDirectory;

    YoriLibInitEmptyString(&AppDirectory);

    if (InstallDirectory == NULL) {
        if (!YoriPkgGetApplicationDirectory(&AppDirectory)) {
            return FALSE;
        }
    } else {
        if (!YoriLibAllocateString(&AppDirectory, InstallDirectory->LengthInChars + MAX_PATH)) {
            return FALSE;
        }
        memcpy(AppDirectory.StartOfString, InstallDirectory->StartOfString, InstallDirectory->LengthInChars * sizeof(TCHAR));
        AppDirectory.StartOfString[InstallDirectory->LengthInChars] = '\0';
        AppDirectory.LengthInChars = InstallDirectory->LengthInChars;
    }

    if (AppDirectory.LengthAllocated - AppDirectory.LengthInChars < sizeof("\\packages.ini")) {
        YoriLibFreeStringContents(&AppDirectory);
        return FALSE;
    }

    YoriLibSPrintf(&AppDirectory.StartOfString[AppDirectory.LengthInChars], _T("\\packages.ini"));
    AppDirectory.LengthInChars += sizeof("\\packages.ini") - 1;

    memcpy(IniFileName, &AppDirectory, sizeof(YORI_STRING));
    return TRUE;
}

/**
 Given a fully qualified path to a package's INI file, extract package
 information.

 @param IniPath Pointer to a path to the package's INI file.  Note this is the
        file embedded in each package, not the global INI file.

 @param PackageName On successful completion, populated with the package's
        canonical name.

 @param PackageVersion On successful completion, populated with the package
        version.

 @param PackageArch On successful completion, populated with the package
        architecture.

 @param MinimumOSBuild On successful completion, populated with the minimum
        OS build needed to install the package.

 @param PackagePathForOlderBuilds On successful completion, populated with a
        URL that will return a version of the package suitable for use on
        systems with less than MinimumOSBuild.

 @param UpgradePath On successful completion, populated with a URL that will
        return the latest version of the package.

 @param SourcePath On successful completion, populated with a URL that will
        return the source code matching the package.

 @param SymbolPath On successful completion, populated with a URL that will
        return the symbols for the package.

 @param UpgradeToDailyPath On successful completion, populated with a URL
        that will return the latest daily version of the package.

 @param UpgradeToStablePath On successful completion, populated with a URL
        that will return the latest stable version of the package.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOL
YoriPkgGetPackageInfo(
    __in PCYORI_STRING IniPath,
    __out PYORI_STRING PackageName,
    __out PYORI_STRING PackageVersion,
    __out PYORI_STRING PackageArch,
    __out PYORI_STRING MinimumOSBuild,
    __out PYORI_STRING PackagePathForOlderBuilds,
    __out PYORI_STRING UpgradePath,
    __out PYORI_STRING SourcePath,
    __out PYORI_STRING SymbolPath,
    __out PYORI_STRING UpgradeToDailyPath,
    __out PYORI_STRING UpgradeToStablePath
    )
{
    YORI_STRING TempBuffer;
    DWORD MaxFieldSize = YORIPKG_MAX_FIELD_LENGTH;

    if (!YoriLibAllocateString(&TempBuffer, 10 * MaxFieldSize)) {
        return FALSE;
    }

    YoriLibCloneString(PackageName, &TempBuffer);
    PackageName->LengthAllocated = MaxFieldSize;

    PackageName->LengthInChars = GetPrivateProfileString(_T("Package"), _T("Name"), _T(""), PackageName->StartOfString, PackageName->LengthAllocated, IniPath->StartOfString);

    YoriLibCloneString(PackageVersion, &TempBuffer);
    PackageVersion->StartOfString += 1 * MaxFieldSize;
    PackageVersion->LengthAllocated = MaxFieldSize;

    PackageVersion->LengthInChars = GetPrivateProfileString(_T("Package"), _T("Version"), _T(""), PackageVersion->StartOfString, PackageVersion->LengthAllocated, IniPath->StartOfString);

    YoriLibCloneString(PackageArch, &TempBuffer);
    PackageArch->StartOfString += 2 * MaxFieldSize;
    PackageArch->LengthAllocated = MaxFieldSize;

    PackageArch->LengthInChars = GetPrivateProfileString(_T("Package"), _T("Architecture"), _T(""), PackageArch->StartOfString, PackageArch->LengthAllocated, IniPath->StartOfString);

    YoriLibCloneString(MinimumOSBuild, &TempBuffer);
    MinimumOSBuild->StartOfString += 3 * MaxFieldSize;
    MinimumOSBuild->LengthAllocated = MaxFieldSize;

    MinimumOSBuild->LengthInChars = GetPrivateProfileString(_T("Package"), _T("MinimumOSBuild"), _T(""), MinimumOSBuild->StartOfString, MinimumOSBuild->LengthAllocated, IniPath->StartOfString);

    YoriLibCloneString(PackagePathForOlderBuilds, &TempBuffer);
    PackagePathForOlderBuilds->StartOfString += 4 * MaxFieldSize;
    PackagePathForOlderBuilds->LengthAllocated = MaxFieldSize;

    PackagePathForOlderBuilds->LengthInChars = GetPrivateProfileString(_T("Package"), _T("PackagePathForOlderBuilds"), _T(""), PackagePathForOlderBuilds->StartOfString, PackagePathForOlderBuilds->LengthAllocated, IniPath->StartOfString);

    YoriLibCloneString(UpgradePath, &TempBuffer);
    UpgradePath->StartOfString += 5 * MaxFieldSize;
    UpgradePath->LengthAllocated = MaxFieldSize;

    UpgradePath->LengthInChars = GetPrivateProfileString(_T("Package"), _T("UpgradePath"), _T(""), UpgradePath->StartOfString, UpgradePath->LengthAllocated, IniPath->StartOfString);

    YoriLibCloneString(SourcePath, &TempBuffer);
    SourcePath->StartOfString += 6 * MaxFieldSize;
    SourcePath->LengthAllocated = MaxFieldSize;

    SourcePath->LengthInChars = GetPrivateProfileString(_T("Package"), _T("SourcePath"), _T(""), SourcePath->StartOfString, SourcePath->LengthAllocated, IniPath->StartOfString);

    YoriLibCloneString(SymbolPath, &TempBuffer);
    SymbolPath->StartOfString += 7 * MaxFieldSize;
    SymbolPath->LengthAllocated = MaxFieldSize;

    SymbolPath->LengthInChars = GetPrivateProfileString(_T("Package"), _T("SymbolPath"), _T(""), SymbolPath->StartOfString, SymbolPath->LengthAllocated, IniPath->StartOfString);

    YoriLibCloneString(UpgradeToDailyPath, &TempBuffer);
    UpgradeToDailyPath->StartOfString += 8 * MaxFieldSize;
    UpgradeToDailyPath->LengthAllocated = MaxFieldSize;

    UpgradeToDailyPath->LengthInChars = GetPrivateProfileString(_T("Package"), _T("UpgradeToDailyPath"), _T(""), UpgradeToDailyPath->StartOfString, UpgradeToDailyPath->LengthAllocated, IniPath->StartOfString);

    YoriLibCloneString(UpgradeToStablePath, &TempBuffer);
    UpgradeToStablePath->StartOfString += 9 * MaxFieldSize;
    UpgradeToStablePath->LengthAllocated = MaxFieldSize;

    UpgradeToStablePath->LengthInChars = GetPrivateProfileString(_T("Package"), _T("UpgradeToStablePath"), _T(""), UpgradeToStablePath->StartOfString, UpgradeToStablePath->LengthAllocated, IniPath->StartOfString);

    YoriLibFreeStringContents(&TempBuffer);
    return TRUE;
}

/**
 Given a fully qualified path to the system package INI file and a package
 name, extract fixed sized information.

 @param IniPath Pointer to a path to the system's INI file.

 @param PackageName Pointer to the package's canonical name.

 @param PackageVersion On successful completion, populated with the package
        version.

 @param PackageArch On successful completion, populated with the package
        architecture.

 @param UpgradePath On successful completion, populated with a URL that will
        return the latest version of the package.

 @param SourcePath On successful completion, populated with a URL that will
        return the source code matching the package.

 @param SymbolPath On successful completion, populated with a URL that will
        return the symbols for the package.

 @param UpgradeToDailyPath On successful completion, populated with a URL
        that will return the latest daily version of the package.

 @param UpgradeToStablePath On successful completion, populated with a URL
        that will return the latest stable version of the package.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOL
YoriPkgGetInstalledPackageInfo(
    __in PYORI_STRING IniPath,
    __in PYORI_STRING PackageName,
    __out PYORI_STRING PackageVersion,
    __out PYORI_STRING PackageArch,
    __out PYORI_STRING UpgradePath,
    __out PYORI_STRING SourcePath,
    __out PYORI_STRING SymbolPath,
    __out PYORI_STRING UpgradeToDailyPath,
    __out PYORI_STRING UpgradeToStablePath
    )
{
    YORI_STRING TempBuffer;
    DWORD MaxFieldSize = YORIPKG_MAX_FIELD_LENGTH;

    ASSERT(YoriLibIsStringNullTerminated(IniPath));
    ASSERT(YoriLibIsStringNullTerminated(PackageName));

    if (!YoriLibAllocateString(&TempBuffer, 7 * MaxFieldSize)) {
        return FALSE;
    }

    YoriLibCloneString(PackageVersion, &TempBuffer);
    PackageVersion->LengthAllocated = MaxFieldSize;

    PackageVersion->LengthInChars = GetPrivateProfileString(PackageName->StartOfString, _T("Version"), _T(""), PackageVersion->StartOfString, PackageVersion->LengthAllocated, IniPath->StartOfString);

    YoriLibCloneString(PackageArch, &TempBuffer);
    PackageArch->StartOfString += 1 * MaxFieldSize;
    PackageArch->LengthAllocated = MaxFieldSize;

    PackageArch->LengthInChars = GetPrivateProfileString(PackageName->StartOfString, _T("Architecture"), _T(""), PackageArch->StartOfString, PackageArch->LengthAllocated, IniPath->StartOfString);

    YoriLibCloneString(UpgradePath, &TempBuffer);
    UpgradePath->StartOfString += 2 * MaxFieldSize;
    UpgradePath->LengthAllocated = MaxFieldSize;

    UpgradePath->LengthInChars = GetPrivateProfileString(PackageName->StartOfString, _T("UpgradePath"), _T(""), UpgradePath->StartOfString, UpgradePath->LengthAllocated, IniPath->StartOfString);

    YoriLibCloneString(SourcePath, &TempBuffer);
    SourcePath->StartOfString += 3 * MaxFieldSize;
    SourcePath->LengthAllocated = MaxFieldSize;

    SourcePath->LengthInChars = GetPrivateProfileString(PackageName->StartOfString, _T("SourcePath"), _T(""), SourcePath->StartOfString, SourcePath->LengthAllocated, IniPath->StartOfString);

    YoriLibCloneString(SymbolPath, &TempBuffer);
    SymbolPath->StartOfString += 4 * MaxFieldSize;
    SymbolPath->LengthAllocated = MaxFieldSize;

    SymbolPath->LengthInChars = GetPrivateProfileString(PackageName->StartOfString, _T("SymbolPath"), _T(""), SymbolPath->StartOfString, SymbolPath->LengthAllocated, IniPath->StartOfString);

    YoriLibCloneString(UpgradeToDailyPath, &TempBuffer);
    UpgradeToDailyPath->StartOfString += 5 * MaxFieldSize;
    UpgradeToDailyPath->LengthAllocated = MaxFieldSize;

    UpgradeToDailyPath->LengthInChars = GetPrivateProfileString(PackageName->StartOfString, _T("UpgradeToDailyPath"), _T(""), UpgradeToDailyPath->StartOfString, UpgradeToDailyPath->LengthAllocated, IniPath->StartOfString);

    YoriLibCloneString(UpgradeToStablePath, &TempBuffer);
    UpgradeToStablePath->StartOfString += 5 * MaxFieldSize;
    UpgradeToStablePath->LengthAllocated = MaxFieldSize;

    UpgradeToStablePath->LengthInChars = GetPrivateProfileString(PackageName->StartOfString, _T("UpgradeToStablePath"), _T(""), UpgradeToStablePath->StartOfString, UpgradeToStablePath->LengthAllocated, IniPath->StartOfString);

    YoriLibFreeStringContents(&TempBuffer);
    return TRUE;
}

/**
 A structure identifying the mapping from one source location to a mirrored
 location.
 */
typedef struct _YORIPKG_MIRROR {

    /**
     The item of this mirror within a list of mirrors.
     */
    YORI_LIST_ENTRY MirrorList;

    /**
     The source path, meaning the path that packages refer to.
     */
    YORI_STRING SourceName;

    /**
     The target path, meaning the path that should be used instead of the
     path contained in packages.
     */
    YORI_STRING TargetName;
} YORIPKG_MIRROR, *PYORIPKG_MIRROR;

/**
 Load mirrors from the Ini file into a list.

 @param IniFilePath Pointer to the path to the packages.ini file.

 @param MirrorsList On input, contains an initialized list, which will be
        populated with mirrors found in the INI file.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriPkgLoadMirrorsFromIni(
    __in PYORI_STRING IniFilePath,
    __inout PYORI_LIST_ENTRY MirrorsList
    )
{
    YORI_STRING IniSection;
    YORI_STRING Find;
    YORI_STRING Replace;
    LPTSTR ThisLine;
    LPTSTR Equals;
    BOOL Result = FALSE;
    DWORD Index;
    PYORIPKG_MIRROR Mirror;

    YoriLibInitEmptyString(&IniSection);
    YoriLibInitEmptyString(&Find);
    YoriLibInitEmptyString(&Replace);

    if (!YoriLibAllocateString(&IniSection, YORIPKG_MAX_SECTION_LENGTH)) {
        goto Exit;
    }

    IniSection.LengthInChars = GetPrivateProfileSection(_T("Mirrors"), IniSection.StartOfString, IniSection.LengthAllocated, IniFilePath->StartOfString);

    ThisLine = IniSection.StartOfString;

    while (*ThisLine != '\0') {
        Find.StartOfString = ThisLine;
        Equals = _tcschr(ThisLine, '=');
        if (Equals == NULL) {
            ThisLine += _tcslen(ThisLine);
            ThisLine++;
            continue;
        }

        Find.LengthInChars = (DWORD)(Equals - ThisLine);

        //
        //  In order to allow '=' to be in the find/replace strings, which are
        //  not describable in an INI file, interpret '%' as '='.
        //

        for (Index = 0; Index < Find.LengthInChars; Index++) {
            if (Find.StartOfString[Index] == '%') {
                Find.StartOfString[Index] = '=';
            }
        }

        Replace.StartOfString = Equals + 1;
        Replace.LengthInChars = _tcslen(Replace.StartOfString);

        ThisLine += Find.LengthInChars + 1 + Replace.LengthInChars + 1;

        Equals[0] = '\0';

        //
        //  In order to allow '=' to be in the find/replace strings, which are
        //  not describable in an INI file, interpret '%' as '='.
        //

        for (Index = 0; Index < Replace.LengthInChars; Index++) {
            if (Replace.StartOfString[Index] == '%') {
                Replace.StartOfString[Index] = '=';
            }
        }

        Mirror = YoriLibReferencedMalloc(sizeof(YORIPKG_MIRROR) + (Find.LengthInChars + 1 + Replace.LengthInChars + 1) * sizeof(TCHAR));
        if (Mirror == NULL) {
            goto Exit;
        }

        YoriLibInitEmptyString(&Mirror->SourceName);
        Mirror->SourceName.StartOfString = (LPTSTR)(Mirror + 1);
        Mirror->SourceName.LengthInChars = Find.LengthInChars;
        memcpy(Mirror->SourceName.StartOfString, Find.StartOfString, Find.LengthInChars * sizeof(TCHAR));
        Mirror->SourceName.StartOfString[Mirror->SourceName.LengthInChars] = '\0';
        Mirror->SourceName.LengthAllocated = Mirror->SourceName.LengthInChars + 1;
        YoriLibInitEmptyString(&Mirror->TargetName);
        Mirror->TargetName.StartOfString = (LPTSTR)(Mirror->SourceName.StartOfString + Mirror->SourceName.LengthAllocated);
        Mirror->TargetName.LengthInChars = Replace.LengthInChars;
        memcpy(Mirror->TargetName.StartOfString, Replace.StartOfString, Replace.LengthInChars * sizeof(TCHAR));
        Mirror->TargetName.StartOfString[Mirror->TargetName.LengthInChars] = '\0';
        Mirror->TargetName.LengthAllocated = Mirror->TargetName.LengthInChars + 1;

        YoriLibAppendList(MirrorsList, &Mirror->MirrorList);
    }

    Result = TRUE;

Exit:
    YoriLibFreeStringContents(&IniSection);
    return Result;
}

/**
 Free a list of allocated mirrors.

 @param MirrorsList Pointer to the list of mirrors to free.
 */
VOID
YoriPkgFreeMirrorList(
    __in PYORI_LIST_ENTRY MirrorsList
    )
{
    PYORI_LIST_ENTRY MirrorEntry;
    PYORIPKG_MIRROR Mirror;

    MirrorEntry = NULL;
    MirrorEntry = YoriLibGetNextListEntry(MirrorsList, MirrorEntry);
    while (MirrorEntry != NULL) {
        Mirror = CONTAINING_RECORD(MirrorEntry, YORIPKG_MIRROR, MirrorList);
        MirrorEntry = YoriLibGetNextListEntry(MirrorsList, MirrorEntry);
        YoriLibRemoveListItem(&Mirror->MirrorList);
        YoriLibDereference(Mirror);
    }

}

/**
 Read the current list of mirrors from the system global packages.ini file
 and display the result to output.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriPkgDisplayMirrors(VOID)
{
    YORI_LIST_ENTRY MirrorsList;
    PYORI_LIST_ENTRY MirrorEntry;
    PYORIPKG_MIRROR Mirror;
    YORI_STRING PackagesIni;

    YoriLibInitializeListHead(&MirrorsList);

    if (!YoriPkgGetPackageIniFile(NULL, &PackagesIni)) {
        return FALSE;
    }

    if (!YoriPkgLoadMirrorsFromIni(&PackagesIni, &MirrorsList)) {
        YoriLibFreeStringContents(&PackagesIni);
        return TRUE;
    }

    YoriLibFreeStringContents(&PackagesIni);

    //
    //  Display the mirrors we found.
    //

    MirrorEntry = NULL;
    MirrorEntry = YoriLibGetNextListEntry(&MirrorsList, MirrorEntry);
    while (MirrorEntry != NULL) {
        Mirror = CONTAINING_RECORD(MirrorEntry, YORIPKG_MIRROR, MirrorList);
        MirrorEntry = YoriLibGetNextListEntry(&MirrorsList, MirrorEntry);
        YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%y %y\n"), &Mirror->SourceName, &Mirror->TargetName);
    }

    //
    //  Free the mirrors we found.
    //

    YoriPkgFreeMirrorList(&MirrorsList);
    return TRUE;
}

/**
 Install a new mirror and add it to packages.ini

 @param SourceName The new source path that is being mirrored

 @param TargetName The new target containing mirrored contents

 @param InstallAsFirst If TRUE, the new source is added to the beginning of
        the list.  If FALSE, it is added to the end.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriPkgAddNewMirror(
    __in PYORI_STRING SourceName,
    __in PYORI_STRING TargetName,
    __in BOOLEAN InstallAsFirst
    )
{
    YORI_LIST_ENTRY MirrorsList;
    PYORI_LIST_ENTRY MirrorEntry;
    PYORIPKG_MIRROR Mirror;
    PYORIPKG_MIRROR NewMirror;
    YORI_STRING PackagesIni;
    DWORD Index;

    YoriLibInitializeListHead(&MirrorsList);

    if (!YoriPkgGetPackageIniFile(NULL, &PackagesIni)) {
        return FALSE;
    }

    if (!YoriPkgLoadMirrorsFromIni(&PackagesIni, &MirrorsList)) {
        YoriLibFreeStringContents(&PackagesIni);
        return FALSE;
    }

    //
    //  Allocate a new mirror entry for the user's request.
    //

    NewMirror = YoriLibReferencedMalloc(sizeof(YORIPKG_MIRROR) + (SourceName->LengthInChars + 1 + TargetName->LengthInChars + 1) * sizeof(TCHAR));
    if (NewMirror == NULL) {
        YoriLibFreeStringContents(&PackagesIni);
        return FALSE;
    }

    YoriLibInitEmptyString(&NewMirror->SourceName);
    NewMirror->SourceName.StartOfString = (LPTSTR)(NewMirror + 1);
    NewMirror->SourceName.LengthInChars = SourceName->LengthInChars;
    memcpy(NewMirror->SourceName.StartOfString, SourceName->StartOfString, SourceName->LengthInChars * sizeof(TCHAR));
    NewMirror->SourceName.StartOfString[NewMirror->SourceName.LengthInChars] = '\0';
    NewMirror->SourceName.LengthAllocated = NewMirror->SourceName.LengthInChars + 1;
    YoriLibInitEmptyString(&NewMirror->TargetName);
    NewMirror->TargetName.StartOfString = (LPTSTR)(NewMirror->SourceName.StartOfString + NewMirror->SourceName.LengthAllocated);
    NewMirror->TargetName.LengthInChars = TargetName->LengthInChars;
    memcpy(NewMirror->TargetName.StartOfString, TargetName->StartOfString, TargetName->LengthInChars * sizeof(TCHAR));
    NewMirror->TargetName.StartOfString[NewMirror->TargetName.LengthInChars] = '\0';
    NewMirror->TargetName.LengthAllocated = NewMirror->TargetName.LengthInChars + 1;

    //
    //  Replace = with %
    //

    for (Index = 0; Index < NewMirror->SourceName.LengthInChars; Index++) {
        if (NewMirror->SourceName.StartOfString[Index] == '=') {
            NewMirror->SourceName.StartOfString[Index] = '%';
        }
    }

    for (Index = 0; Index < NewMirror->TargetName.LengthInChars; Index++) {
        if (NewMirror->TargetName.StartOfString[Index] == '=') {
            NewMirror->TargetName.StartOfString[Index] = '%';
        }
    }

    //
    //  Go through the list.  If we find a matching entry, remove it.
    //

    MirrorEntry = NULL;
    MirrorEntry = YoriLibGetNextListEntry(&MirrorsList, MirrorEntry);
    while (MirrorEntry != NULL) {
        Mirror = CONTAINING_RECORD(MirrorEntry, YORIPKG_MIRROR, MirrorList);
        MirrorEntry = YoriLibGetNextListEntry(&MirrorsList, MirrorEntry);

        //
        //  Convert each loaded entry back into its storage format.  We need
        //  to do this before writing them, and need the entries to be
        //  consistent for the compare.
        //

        for (Index = 0; Index < Mirror->SourceName.LengthInChars; Index++) {
            if (Mirror->SourceName.StartOfString[Index] == '=') {
                Mirror->SourceName.StartOfString[Index] = '%';
            }
        }

        for (Index = 0; Index < Mirror->TargetName.LengthInChars; Index++) {
            if (Mirror->TargetName.StartOfString[Index] == '=') {
                Mirror->TargetName.StartOfString[Index] = '%';
            }
        }

        if (YoriLibCompareString(&Mirror->SourceName, &NewMirror->SourceName) == 0) {
            YoriLibRemoveListItem(&Mirror->MirrorList);
            YoriLibDereference(Mirror);
        }
    }

    //
    //  Insert the new item at the beginning or end per request
    //

    if (InstallAsFirst) {
        YoriLibInsertList(&MirrorsList, &NewMirror->MirrorList);
    } else {
        YoriLibAppendList(&MirrorsList, &NewMirror->MirrorList);
    }

    //
    //  Rewrite the section
    //

    WritePrivateProfileString(_T("Mirrors"), NULL, NULL, PackagesIni.StartOfString);
    MirrorEntry = NULL;
    MirrorEntry = YoriLibGetNextListEntry(&MirrorsList, MirrorEntry);
    while (MirrorEntry != NULL) {
        Mirror = CONTAINING_RECORD(MirrorEntry, YORIPKG_MIRROR, MirrorList);
        MirrorEntry = YoriLibGetNextListEntry(&MirrorsList, MirrorEntry);
        WritePrivateProfileString(_T("Mirrors"), Mirror->SourceName.StartOfString, Mirror->TargetName.StartOfString, PackagesIni.StartOfString);
    }

    //
    //  Free the mirrors we found.
    //

    YoriPkgFreeMirrorList(&MirrorsList);
    YoriLibFreeStringContents(&PackagesIni);
    return TRUE;
}

/**
 Delete a mirror from packages.ini

 @param SourceName The source path that was being mirrored

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YoriPkgDeleteMirror(
    __in PYORI_STRING SourceName
    )
{
    YORI_LIST_ENTRY MirrorsList;
    PYORI_LIST_ENTRY MirrorEntry;
    PYORIPKG_MIRROR Mirror;
    YORI_STRING PackagesIni;
    DWORD Index;

    YoriLibInitializeListHead(&MirrorsList);

    if (!YoriPkgGetPackageIniFile(NULL, &PackagesIni)) {
        return FALSE;
    }

    if (!YoriPkgLoadMirrorsFromIni(&PackagesIni, &MirrorsList)) {
        YoriLibFreeStringContents(&PackagesIni);
        return FALSE;
    }

    //
    //  Go through the list.  If we find a matching entry, remove it.
    //

    MirrorEntry = NULL;
    MirrorEntry = YoriLibGetNextListEntry(&MirrorsList, MirrorEntry);
    while (MirrorEntry != NULL) {
        Mirror = CONTAINING_RECORD(MirrorEntry, YORIPKG_MIRROR, MirrorList);
        MirrorEntry = YoriLibGetNextListEntry(&MirrorsList, MirrorEntry);

        if (YoriLibCompareString(&Mirror->SourceName, SourceName) == 0) {
            YoriLibRemoveListItem(&Mirror->MirrorList);
            YoriLibDereference(Mirror);
        } else {

            //
            //  Convert each loaded entry back into its storage format.
            //

            for (Index = 0; Index < Mirror->SourceName.LengthInChars; Index++) {
                if (Mirror->SourceName.StartOfString[Index] == '=') {
                    Mirror->SourceName.StartOfString[Index] = '%';
                }
            }

            for (Index = 0; Index < Mirror->TargetName.LengthInChars; Index++) {
                if (Mirror->TargetName.StartOfString[Index] == '=') {
                    Mirror->TargetName.StartOfString[Index] = '%';
                }
            }
        }
    }

    //
    //  Rewrite the section
    //

    WritePrivateProfileString(_T("Mirrors"), NULL, NULL, PackagesIni.StartOfString);
    MirrorEntry = NULL;
    MirrorEntry = YoriLibGetNextListEntry(&MirrorsList, MirrorEntry);
    while (MirrorEntry != NULL) {
        Mirror = CONTAINING_RECORD(MirrorEntry, YORIPKG_MIRROR, MirrorList);
        MirrorEntry = YoriLibGetNextListEntry(&MirrorsList, MirrorEntry);
        WritePrivateProfileString(_T("Mirrors"), Mirror->SourceName.StartOfString, Mirror->TargetName.StartOfString, PackagesIni.StartOfString);
    }

    //
    //  Free the mirrors we found.
    //

    YoriPkgFreeMirrorList(&MirrorsList);
    YoriLibFreeStringContents(&PackagesIni);
    return TRUE;
}


/**
 Expand a user specified package path into a full path if local, and compare
 the full path or URL against any mirrors in the INI file to determine if
 the path should be adjusted to refer to a mirrored location.

 @param PackagePath Pointer to a string referring to the package which can
        be local or remote.

 @param IniFilePath Pointer to a string containing a path to the package INI
        file.

 @param MirroredPath On successful completion, updated to refer to a
        substituted path from the user's mirrors list or to the full path of
        a local package.

 @return TRUE to indicate a substitution was performed, FALSE on error or
         if no substitution exists.
 */
__success(return)
BOOL
YoriPkgConvertUserPackagePathToMirroredPath(
    __in PYORI_STRING PackagePath,
    __in PCYORI_STRING IniFilePath,
    __out PYORI_STRING MirroredPath
    )
{
    YORI_STRING IniSection;
    YORI_STRING HumanFullPath;
    YORI_STRING Find;
    YORI_STRING Replace;
    LPTSTR ThisLine;
    LPTSTR Equals;
    BOOL Result = FALSE;
    BOOL ReturnHumanPathIfNoMirrorFound = FALSE;
    DWORD Index;

    YoriLibInitEmptyString(&IniSection);
    YoriLibInitEmptyString(&HumanFullPath);
    YoriLibInitEmptyString(MirroredPath);
    YoriLibInitEmptyString(&Find);
    YoriLibInitEmptyString(&Replace);

    if (!YoriLibAllocateString(&IniSection, YORIPKG_MAX_SECTION_LENGTH)) {
        goto Exit;
    }

    if (!YoriLibIsPathUrl(PackagePath)) {
        if (!YoriLibUserStringToSingleFilePath(PackagePath, FALSE, &HumanFullPath)) {
            YoriLibInitEmptyString(&HumanFullPath);
            goto Exit;
        }
        ReturnHumanPathIfNoMirrorFound = TRUE;
    } else {
        YoriLibCloneString(&HumanFullPath, PackagePath);
    }

    IniSection.LengthInChars = GetPrivateProfileSection(_T("Mirrors"), IniSection.StartOfString, IniSection.LengthAllocated, IniFilePath->StartOfString);

    ThisLine = IniSection.StartOfString;

    while (*ThisLine != '\0') {
        Find.StartOfString = ThisLine;
        Equals = _tcschr(ThisLine, '=');
        if (Equals == NULL) {
            ThisLine += _tcslen(ThisLine);
            ThisLine++;
            continue;
        }

        Find.LengthInChars = (DWORD)(Equals - ThisLine);

        //
        //  In order to allow '=' to be in the find/replace strings, which are
        //  not describable in an INI file, interpret '%' as '='.
        //

        for (Index = 0; Index < Find.LengthInChars; Index++) {
            if (Find.StartOfString[Index] == '%') {
                Find.StartOfString[Index] = '=';
            }
        }


        Replace.StartOfString = Equals + 1;
        Replace.LengthInChars = _tcslen(Replace.StartOfString);

        ThisLine += Find.LengthInChars + 1 + Replace.LengthInChars + 1;

        Equals[0] = '\0';

        if (YoriLibCompareStringInsensitiveCount(&Find, &HumanFullPath, Find.LengthInChars) == 0) {
            YORI_STRING SubstringToKeep;
            YoriLibInitEmptyString(&SubstringToKeep);
            SubstringToKeep.StartOfString = &HumanFullPath.StartOfString[Find.LengthInChars];
            SubstringToKeep.LengthInChars = HumanFullPath.LengthInChars - Find.LengthInChars;

            //
            //  In order to allow '=' to be in the find/replace strings, which are
            //  not describable in an INI file, interpret '%' as '='.
            //

            for (Index = 0; Index < Replace.LengthInChars; Index++) {
                if (Replace.StartOfString[Index] == '%') {
                    Replace.StartOfString[Index] = '=';
                }
            }

            YoriLibYPrintf(MirroredPath, _T("%y%y"), &Replace, &SubstringToKeep);
            if (MirroredPath->StartOfString != NULL) {
                YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Converting %y to %y\n"), &HumanFullPath, MirroredPath);
                Result = TRUE;
            }
            goto Exit;
        }

    }

    //
    //  This doesn't really belong here, but since we've already converted the
    //  user string to a full path we can return it so the caller doesn't have
    //  to do it again
    //

    if (ReturnHumanPathIfNoMirrorFound) {
        YoriLibCloneString(MirroredPath, &HumanFullPath);
        Result = TRUE;
    }

Exit:
    YoriLibFreeStringContents(&IniSection);
    YoriLibFreeStringContents(&HumanFullPath);
    return Result;
}

#if defined(_MSC_VER) && (_MSC_VER == 1500)
#pragma warning(push)
#pragma warning(disable: 6309 6387) // GetTempPath is mis-annotated in old SDKs
#endif

/**
 Download a remote package into a temporary location and return the
 temporary location to allow for subsequent processing.

 @param PackagePath Pointer to a string referring to the package which can
        be local or remote.

 @param IniFilePath Pointer to a string containing a path to the package INI
        file.

 @param LocalPath On successful completion, populated with a string containing
        a fully qualified local path to the package.

 @param DeleteWhenFinished On successful completion, set to TRUE to indicate
        the caller should delete the file (it is temporary); set to FALSE to
        indicate the file should be retained.

 @return ERROR_SUCCESS to indicate success, or other Win32 error to indicate
         the type of failure.
 */
__success(return == ERROR_SUCCESS)
DWORD
YoriPkgPackagePathToLocalPath(
    __in PYORI_STRING PackagePath,
    __in_opt PCYORI_STRING IniFilePath,
    __out PYORI_STRING LocalPath,
    __out PBOOL DeleteWhenFinished
    )
{
    YORI_STRING MirroredPath;
    DWORD Result = ERROR_SUCCESS;

    YoriLibInitEmptyString(&MirroredPath);

    //
    //  See if there's a mirror for the package.  If anything goes wrong in
    //  this process, just keep using the original path.  If there's no INI
    //  file, there's no possibility of a mirror, so perform path expansion
    //  on local paths and keep URLs verbatim.
    //

    if (IniFilePath == NULL) {
        if (!YoriLibIsPathUrl(PackagePath)) {
            if (!YoriLibUserStringToSingleFilePath(PackagePath, FALSE, &MirroredPath)) {
                YoriLibCloneString(&MirroredPath, PackagePath);
            }
        } else {
            YoriLibCloneString(&MirroredPath, PackagePath);
        }
    } else if (!YoriPkgConvertUserPackagePathToMirroredPath(PackagePath, IniFilePath, &MirroredPath)) {
        YoriLibCloneString(&MirroredPath, PackagePath);
    }

    if (YoriLibIsPathUrl(&MirroredPath)) {

        YORI_STRING TempPath;
        YORI_STRING TempFileName;
        YORI_STRING UserAgent;
        YoriLibUpdError Error;
        YoriLibInitEmptyString(&TempPath);

        //
        //  Query for a temporary directory
        //

        TempPath.LengthAllocated = GetTempPath(0, NULL);
        if (!YoriLibAllocateString(&TempPath, TempPath.LengthAllocated)) {
            Result = ERROR_NOT_ENOUGH_MEMORY;
            goto Exit;
        }
        TempPath.LengthInChars = GetTempPath(TempPath.LengthAllocated, TempPath.StartOfString);

        if (!YoriLibAllocateString(&TempFileName, MAX_PATH)) {
            YoriLibFreeStringContents(&TempPath);
            Result = ERROR_NOT_ENOUGH_MEMORY;
            goto Exit;
        }

        //
        //  This will attempt to create a temporary file.  If it fails, that
        //  implies the temp directory is not writable, does not exist, or
        //  is unusable for some other reason.
        //

        if (GetTempFileName(TempPath.StartOfString, _T("ypm"), 0, TempFileName.StartOfString) == 0) {
            YoriLibFreeStringContents(&TempPath);
            YoriLibFreeStringContents(&TempFileName);
            Result = ERROR_BAD_ENVIRONMENT;
            goto Exit;
        }

        TempFileName.LengthInChars = _tcslen(TempFileName.StartOfString);

        YoriLibInitEmptyString(&UserAgent);
        YoriLibYPrintf(&UserAgent, _T("ypm %i.%02i\r\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
        if (UserAgent.StartOfString == NULL) {
            YoriLibFreeStringContents(&TempPath);
            YoriLibFreeStringContents(&TempFileName);
            Result = ERROR_NOT_ENOUGH_MEMORY;
            goto Exit;
        }

        Error = YoriLibUpdateBinaryFromUrl(MirroredPath.StartOfString, TempFileName.StartOfString, UserAgent.StartOfString, NULL);

        if (Error != YoriLibUpdErrorSuccess) {
            switch(Error) {
                case YoriLibUpdErrorInetInit:
                case YoriLibUpdErrorInetConnect:
                case YoriLibUpdErrorInetRead:
                case YoriLibUpdErrorInetContents:
                    Result = ERROR_NO_NETWORK;
                    break;
                case YoriLibUpdErrorFileWrite:
                case YoriLibUpdErrorFileReplace:
                    Result = ERROR_WRITE_FAULT;
                    break;
                default:
                    Result = ERROR_NOT_SUPPORTED;
                    break;
            }
        }

        YoriLibFreeStringContents(&TempPath);
        YoriLibFreeStringContents(&UserAgent);

        if (Result != ERROR_SUCCESS) {
            YoriLibFreeStringContents(&TempFileName);
            goto Exit;
        }

        memcpy(LocalPath, &TempFileName, sizeof(YORI_STRING));
        *DeleteWhenFinished = TRUE;

    } else {
        *DeleteWhenFinished = FALSE;
        YoriLibCloneString(LocalPath, &MirroredPath);
    }

Exit:

    YoriLibFreeStringContents(&MirroredPath);
    return Result;
}

#if defined(_MSC_VER) && (_MSC_VER == 1500)
#pragma warning(pop)
#endif

/**
 Display the best available error text given an installation failure with the
 specified Win32 error code.

 @param ErrorCode The error code to display a message for.
 */
VOID
YoriPkgDisplayErrorStringForInstallFailure(
    __in DWORD ErrorCode
    )
{
    LPTSTR ErrText;
    YORI_STRING AdminName;
    BOOL RunningAsAdmin = FALSE;

    switch(ErrorCode) {
        case ERROR_ALREADY_ASSIGNED:
            //
            //  This error means "we already told the user about it in a
            //  more specific way so please do nothing later."
            //
            break;
        case ERROR_WRITE_FAULT:
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Could not write to temporary directory.\n"));
            break;
        case ERROR_NO_NETWORK:
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Could not download package from the network.\n"));
            break;
        case ERROR_BAD_ENVIRONMENT:
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("The temporary directory does not exist or cannot be written to.\n"));
            break;
        case ERROR_ACCESS_DENIED:
            YoriLibConstantString(&AdminName, _T("Administrators"));
            if (!YoriLibIsCurrentUserInGroup(&AdminName, &RunningAsAdmin) || RunningAsAdmin) {
                YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Access denied when writing to files.\n"));
            } else {
                YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Not running as Administrator and could not write to files.  Perhaps elevation is required?\n"));
            }
            break;

        default:
            ErrText = YoriLibGetWinErrorText(ErrorCode);
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("%s"), ErrText);
            YoriLibFreeWinErrorText(ErrText);

    }
}

// vim:sw=4:ts=4:et:


<|start_filename|>ypm/ypmcreat.c<|end_filename|>
/**
 * @file ypm/ypmcreat.c
 *
 * Yori shell package manager create packages
 *
 * Copyright (c) 2018-2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>
#include <yoripkg.h>
#include "ypm.h"

/**
 Help text to display to the user.
 */
const
CHAR strYpmCreateBinaryHelpText[] =
        "\n"
        "Create a binary package.\n"
        "\n"
        "YPM [-license]\n"
        "YPM -c <file> <pkgname> <version> <arch> -filelist <file>\n"
        "       [-minimumosbuild <number>] [-packagepathforolderbuilds <path>]\n"
        "       [-upgradedaily <path>] [-upgradepath <path>] [-upgradestable <path>]\n"
        "       [-sourcepath <path>] [-symbolpath <path>] [-replaces <packages>]\n"
        "\n"
        "   -filelist       Specifies a file containing a list of files to include in\n"
        "                   the package, one per line\n"
        "   -minimumosbuild Specifies the minimum build of NT that can run the package\n"
        "   -packagepathforolderbuilds\n"
        "                   Specifies a URL for a package that can install on builds\n"
        "                   older than minimumosbuild\n"
        "   -replaces       A list of package names that this package should replace\n"
        "   -sourcepath     Specifies a URL containing the source package for this\n"
        "                   binary package\n"
        "   -symbolpath     Specifies a URL containing the symbol package for this\n"
        "                   binary package\n"
        "   -upgradedaily   Specifies a URL containing the latest daily version of\n"
        "                   the package\n"
        "   -upgradepath    Specifies a URL containing the latest version of the\n" 
        "                   package\n"
        "   -upgradestable  Specifies a URL containing the latest stable version of\n"
        "                   the package\n";
/**
 Help text to display to the user.
 */
const
CHAR strYpmCreateSourceHelpText[] =
        "\n"
        "Create a source package.\n"
        "\n"
        "YPM [-license]\n"
        "YPM -cs <file> <pkgname> <version> -filepath <directory>\n"
        "\n"
        "   -filepath       Specifies a directory containing source code\n";

/**
 Display usage text to the user.
 */
BOOL
YpmCreateBinaryHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Ypm %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strYpmCreateBinaryHelpText);
    return TRUE;
}

/**
 Display usage text to the user.
 */
BOOL
YpmCreateSourceHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Ypm %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strYpmCreateSourceHelpText);
    return TRUE;
}

/**
 Create a new package containing binary files that can be installed on a
 user's system.  These packages consist of files provided in a list of files.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process.
 */
DWORD
YpmCreateBinaryPackage(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    YORI_STRING Arg;
    PYORI_STRING NewFileName = NULL;
    PYORI_STRING NewVersion = NULL;
    PYORI_STRING NewArch = NULL;
    PYORI_STRING NewName = NULL;
    PYORI_STRING SourcePath = NULL;
    PYORI_STRING UpgradePath = NULL;
    PYORI_STRING UpgradeToStablePath = NULL;
    PYORI_STRING UpgradeToDailyPath = NULL;
    PYORI_STRING SymbolPath = NULL;
    PYORI_STRING FileList = NULL;
    PYORI_STRING FilePath = NULL;
    PYORI_STRING Replaces = NULL;
    PYORI_STRING MinimumOSBuild = NULL;
    PYORI_STRING PackagePathForOlderBuilds = NULL;
    DWORD ReplaceCount = 0;

    if (ArgC < 5) {
        YpmCreateBinaryHelp();
        return EXIT_FAILURE;
    }

    NewFileName = &ArgV[1];
    NewName = &ArgV[2];
    NewVersion = &ArgV[3];
    NewArch = &ArgV[4];

    for (i = 5; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                YpmCreateBinaryHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2017-2021"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("a")) == 0) {
                if (i + 1 < ArgC) {
                    NewArch = &ArgV[i + 1];
                    i++;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("filelist")) == 0) {
                if (i + 1 < ArgC) {
                    FileList = &ArgV[i + 1];
                    i++;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("filepath")) == 0) {
                if (i + 1 < ArgC) {
                    FilePath = &ArgV[i + 1];
                    i++;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("minimumosbuild")) == 0) {
                if (i + 1 < ArgC) {
                    MinimumOSBuild = &ArgV[i + 1];
                    ArgumentUnderstood = TRUE;
                    i++;
                }

            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("packagepathforolderbuilds")) == 0) {
                if (i + 1 < ArgC) {
                    PackagePathForOlderBuilds = &ArgV[i + 1];
                    ArgumentUnderstood = TRUE;
                    i++;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("replaces")) == 0) {
                if (i + 1 < ArgC) {
                    ArgumentUnderstood = TRUE;
                    Replaces = &ArgV[i + 1];

                    while (i + 1 < ArgC &&
                           !YoriLibIsCommandLineOption(&ArgV[i + 1], &Arg)) {

                        ReplaceCount++;
                        i++;
                    }
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("sourcepath")) == 0) {
                if (i + 1 < ArgC) {
                    SourcePath = &ArgV[i + 1];
                    i++;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("symbolpath")) == 0) {
                if (i + 1 < ArgC) {
                    SymbolPath = &ArgV[i + 1];
                    i++;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("upgradedaily")) == 0) {
                if (i + 1 < ArgC) {
                    UpgradeToDailyPath = &ArgV[i + 1];
                    i++;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("upgradepath")) == 0) {
                if (i + 1 < ArgC) {
                    UpgradePath = &ArgV[i + 1];
                    i++;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("upgradestable")) == 0) {
                if (i + 1 < ArgC) {
                    UpgradeToStablePath = &ArgV[i + 1];
                    i++;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("v")) == 0) {
                if (i + 1 < ArgC) {
                    NewVersion = &ArgV[i + 1];
                    i++;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                ArgumentUnderstood = TRUE;
                StartArg = i + 1;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    ASSERT(NewFileName != NULL && NewName != NULL && NewVersion != NULL && NewArch != NULL);
    if (FileList == NULL) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("ypm: missing file list\n"));
        return EXIT_FAILURE;
    }

    YoriPkgCreateBinaryPackage(NewFileName,
                               NewName,
                               NewVersion,
                               NewArch,
                               FileList,
                               MinimumOSBuild,
                               PackagePathForOlderBuilds,
                               UpgradePath,
                               SourcePath,
                               SymbolPath,
                               UpgradeToStablePath,
                               UpgradeToDailyPath,
                               Replaces,
                               ReplaceCount);

    return EXIT_SUCCESS;
}

/**
 Create a new package containing source files that can be installed on a
 user's system.  These packages consist of files found in a directory tree,
 while excluding any files specified by .gitignore.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process.
 */
DWORD
YpmCreateSourcePackage(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    YORI_STRING Arg;
    PYORI_STRING NewFileName = NULL;
    PYORI_STRING NewVersion = NULL;
    PYORI_STRING NewName = NULL;
    PYORI_STRING FilePath = NULL;

    if (ArgC < 4) {
        YpmCreateSourceHelp();
        return EXIT_FAILURE;
    }

    NewFileName = &ArgV[1];
    NewName = &ArgV[2];
    NewVersion = &ArgV[3];

    for (i = 4; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                YpmCreateSourceHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2017-2021"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("filepath")) == 0) {
                if (i + 1 < ArgC) {
                    FilePath = &ArgV[i + 1];
                    i++;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                ArgumentUnderstood = TRUE;
                StartArg = i + 1;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    ASSERT(NewFileName != NULL && NewName != NULL && NewVersion != NULL);
    if (FilePath == NULL) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("ypm: missing file tree root\n"));
        return EXIT_FAILURE;
    }
    YoriPkgCreateSourcePackage(NewFileName, NewName, NewVersion, FilePath);

    return EXIT_SUCCESS;
}


// vim:sw=4:ts=4:et:


<|start_filename|>initool/initool.c<|end_filename|>
/**
 * @file initool/initool.c
 *
 * Yori query or set values in INI files
 *
 * Copyright (c) 2018 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 Help text to display to the user.
 */
const
CHAR strIniToolHelpText[] =
        "\n"
        "Query or set values in INI files.\n"
        "\n"
        "INITOOL [-license]\n"
        "INITOOL -d <file> <section> [<key>]\n"
        "INITOOL -l <file> <section>\n"
        "INITOOL -r <file> <section> <key>\n"
        "INITOOL -s <file>\n"
        "INITOOL -w <file> <section> <key> <value>\n"
        "\n"
        "   -d             Delete a specified key from an INI file\n"
        "   -l             List key/value pairs in a specified section from an INI file\n"
        "   -r             Read a specified key from an INI file\n"
        "   -s             List sections in an INI file\n"
        "   -w             Write a specified value to an INI file\n";

/**
 Display usage text to the user.
 */
BOOL
IniToolHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("IniTool %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strIniToolHelpText);
    return TRUE;
}

/**
 Delete a value from an INI file.

 @param UserFileName Pointer to the file name of the INI file.

 @param Section Pointer to the section within the INI file to update.

 @param Key Pointer to the key part of the key value pair.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
IniToolDeleteFromIniFile(
    __in PYORI_STRING UserFileName,
    __in PYORI_STRING Section,
    __in_opt PYORI_STRING Key
    )
{
    YORI_STRING RealFileName;

    if (!YoriLibUserStringToSingleFilePath(UserFileName, FALSE, &RealFileName)) {
        return FALSE;
    }

    if (!WritePrivateProfileString(Section->StartOfString, (Key != NULL)?Key->StartOfString:NULL, NULL, RealFileName.StartOfString)) {
        YoriLibFreeStringContents(&RealFileName);
        return FALSE;
    }

    YoriLibFreeStringContents(&RealFileName);
    return TRUE;
}

/**
 List a section from an INI file and write it to standard output.

 @param UserFileName Pointer to the file name of the INI file.

 @param Section Pointer to the section within the INI file to read.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
IniToolListSectionFromIniFile(
    __in PYORI_STRING UserFileName,
    __in PYORI_STRING Section
    )
{
    YORI_STRING RealFileName;
    YORI_STRING Value;
    LPTSTR ThisVar;

    if (!YoriLibUserStringToSingleFilePath(UserFileName, FALSE, &RealFileName)) {
        return FALSE;
    }

    if (!YoriLibAllocateString(&Value, 64 * 1024)) {
        YoriLibFreeStringContents(&RealFileName);
        return FALSE;
    }

    Value.LengthInChars = GetPrivateProfileSection(Section->StartOfString, Value.StartOfString, Value.LengthAllocated, RealFileName.StartOfString);
    ThisVar = Value.StartOfString;
    while (*ThisVar != '\0') {
        YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%s\n"), ThisVar);

        ThisVar += _tcslen(ThisVar);
        ThisVar++;
    }

    YoriLibFreeStringContents(&RealFileName);
    YoriLibFreeStringContents(&Value);
    return TRUE;
}

/**
 List sections from an INI file and write them to standard output.

 @param UserFileName Pointer to the file name of the INI file.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
IniToolListSectionsFromIniFile(
    __in PYORI_STRING UserFileName
    )
{
    YORI_STRING RealFileName;
    YORI_STRING Value;
    LPTSTR ThisVar;

    if (DllKernel32.pGetPrivateProfileSectionNamesW == NULL) {
        return FALSE;
    }

    if (!YoriLibUserStringToSingleFilePath(UserFileName, FALSE, &RealFileName)) {
        return FALSE;
    }

    if (!YoriLibAllocateString(&Value, 64 * 1024)) {
        YoriLibFreeStringContents(&RealFileName);
        return FALSE;
    }

    Value.LengthInChars = DllKernel32.pGetPrivateProfileSectionNamesW(Value.StartOfString, Value.LengthAllocated, RealFileName.StartOfString);
    ThisVar = Value.StartOfString;
    while (*ThisVar != '\0') {
        YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%s\n"), ThisVar);

        ThisVar += _tcslen(ThisVar);
        ThisVar++;
    }

    YoriLibFreeStringContents(&RealFileName);
    YoriLibFreeStringContents(&Value);
    return TRUE;
}

/**
 Read a value from an INI file and write it to standard output.

 @param UserFileName Pointer to the file name of the INI file.

 @param Section Pointer to the section within the INI file to read.

 @param Key Pointer to the key part of the key value pair.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
IniToolReadFromIniFile(
    __in PYORI_STRING UserFileName,
    __in PYORI_STRING Section,
    __in PYORI_STRING Key
    )
{
    YORI_STRING RealFileName;
    YORI_STRING Value;

    if (!YoriLibUserStringToSingleFilePath(UserFileName, FALSE, &RealFileName)) {
        return FALSE;
    }

    if (!YoriLibAllocateString(&Value, 16 * 1024)) {
        YoriLibFreeStringContents(&RealFileName);
        return FALSE;
    }

    Value.LengthInChars = GetPrivateProfileString(Section->StartOfString, Key->StartOfString, _T(""), Value.StartOfString, Value.LengthAllocated, RealFileName.StartOfString);
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%y"), &Value);

    YoriLibFreeStringContents(&RealFileName);
    YoriLibFreeStringContents(&Value);
    return TRUE;
}

/**
 Write a value to an INI file.

 @param UserFileName Pointer to the file name of the INI file.

 @param Section Pointer to the section within the INI file to update.

 @param Key Pointer to the key part of the key value pair.

 @param Value Pointer to the new value to set into the file.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
IniToolWriteToIniFile(
    __in PYORI_STRING UserFileName,
    __in PYORI_STRING Section,
    __in PYORI_STRING Key,
    __in PYORI_STRING Value
    )
{
    YORI_STRING RealFileName;

    if (!YoriLibUserStringToSingleFilePath(UserFileName, FALSE, &RealFileName)) {
        return FALSE;
    }

    if (!WritePrivateProfileString(Section->StartOfString, Key->StartOfString, Value->StartOfString, RealFileName.StartOfString)) {
        YoriLibFreeStringContents(&RealFileName);
        return FALSE;
    }

    YoriLibFreeStringContents(&RealFileName);
    return TRUE;
}

/**
 A list of operations that the tool can perform.
 */
typedef enum _INITOOL_OPERATION {
    IniToolOpNone = 0,
    IniToolOpWriteValue = 1,
    IniToolOpReadValue = 2,
    IniToolOpDeleteValue = 3,
    IniToolOpListSection = 4,
    IniToolOpListSections = 5
} INITOOL_OPERATION;

#ifdef YORI_BUILTIN
/**
 The main entrypoint for the initool builtin command.
 */
#define ENTRYPOINT YoriCmd_INITOOL
#else
/**
 The main entrypoint for the initool standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the ini tool cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the child process on success, or failure if the child
         could not be launched.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    YORI_STRING Arg;
    INITOOL_OPERATION Op;

    Op = IniToolOpNone;

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                IniToolHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2018"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("d")) == 0) {
                Op = IniToolOpDeleteValue;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("l")) == 0) {
                Op = IniToolOpListSection;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("r")) == 0) {
                Op = IniToolOpReadValue;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("s")) == 0) {
                Op = IniToolOpListSections;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("w")) == 0) {
                Op = IniToolOpWriteValue;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                ArgumentUnderstood = TRUE;
                StartArg = i + 1;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    if (Op == IniToolOpNone) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("initool: missing operation\n"));
        return EXIT_FAILURE;
    }

    if (Op == IniToolOpWriteValue) {
        if (StartArg == 0 || StartArg + 4 > ArgC) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("initool: missing argument\n"));
            return EXIT_FAILURE;
        }

        if (!IniToolWriteToIniFile(&ArgV[StartArg], &ArgV[StartArg + 1], &ArgV[StartArg + 2], &ArgV[StartArg + 3])) {
            return EXIT_FAILURE;
        }
    } else if (Op == IniToolOpReadValue) {
        if (StartArg == 0 || StartArg + 3 > ArgC) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("initool: missing argument\n"));
            return EXIT_FAILURE;
        }

        if (!IniToolReadFromIniFile(&ArgV[StartArg], &ArgV[StartArg + 1], &ArgV[StartArg + 2])) {
            return EXIT_FAILURE;
        }
    } else if (Op == IniToolOpDeleteValue) {
        if (StartArg == 0 || StartArg + 2 > ArgC) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("initool: missing argument\n"));
            return EXIT_FAILURE;
        }

        if (StartArg + 2 < ArgC) {

            if (!IniToolDeleteFromIniFile(&ArgV[StartArg], &ArgV[StartArg + 1], &ArgV[StartArg + 2])) {
                return EXIT_FAILURE;
            }
        } else {
            if (!IniToolDeleteFromIniFile(&ArgV[StartArg], &ArgV[StartArg + 1], NULL)) {
                return EXIT_FAILURE;
            }
        }
    } else if (Op == IniToolOpListSection) {
        if (StartArg == 0 || StartArg + 2 > ArgC) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("initool: missing argument\n"));
            return EXIT_FAILURE;
        }

        if (!IniToolListSectionFromIniFile(&ArgV[StartArg], &ArgV[StartArg + 1])) {
            return EXIT_FAILURE;
        }
    } else if (Op == IniToolOpListSections) {
        if (StartArg == 0 || StartArg + 1 > ArgC) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("initool: missing argument\n"));
            return EXIT_FAILURE;
        }

        if (!IniToolListSectionsFromIniFile(&ArgV[StartArg])) {
            return EXIT_FAILURE;
        }
    }

    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>df/df.c<|end_filename|>
/**
 * @file df/df.c
 *
 * Yori shell display disk free on volumes
 *
 * Copyright (c) 2017-2019 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, DFESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 Help text to display to the user.
 */
const
CHAR strDfHelpText[] =
        "\n"
        "Display disk free space.\n"
        "\n"
        "DF [-license] [-m] [<drive>]\n"
        "\n"
        "   -m             Minimal display, raw data only\n";

/**
 Display usage text to the user.
 */
BOOL
DfHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Df %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strDfHelpText);
    return TRUE;
}

/**
 Context structure passed between each drive whose free space is being
 displayed.
 */
typedef struct _DF_CONTEXT {

    /**
     TRUE if the display should be minimal so that it can be easily parsed,
     with no human readability added.
     */
    BOOL MinimalDisplay;

    /**
     TRUE if a graph of space utilization should be displayed.
     */
    BOOL DisplayGraph;

    /**
     The width of the console, in characters.
     */
    DWORD ConsoleWidth;

    /**
     Number of volumes reported.
     */
    DWORD VolumesDisplayed;

    /**
     The color to display file sizes in.
     */
    YORILIB_COLOR_ATTRIBUTES FileSizeColor;

    /**
     A string form of the VT sequence for the file color above.
     */
    YORI_STRING FileSizeColorString;

    /**
     The buffer for the above string.
     */
    TCHAR FileSizeColorStringBuffer[YORI_MAX_INTERNAL_VT_ESCAPE_CHARS];

    /**
     A buffer to generate the graph line into.  This is allocated when the
     app starts and contains ConsoleWidth chars plus space for two VT100
     escape sequences to initiate and terminate the color of the graph.
     */
    YORI_STRING LineBuffer;

    /**
     Color information to display against matching directories.
     */
    YORI_LIB_FILE_FILTER ColorRules;

} DF_CONTEXT, *PDF_CONTEXT;

/**
 Report the space usage on a single volume.

 @param VolName The volume name.  This can either be a volume GUID path
        returned from volume enumeration, or a user specified path to
        anything.

 @param DfContext Pointer to the context used to indicate options for each
        drive whose free space is being reported.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
DfReportSingleVolume(
    __in LPTSTR VolName,
    __in PDF_CONTEXT DfContext
    )
{
    LARGE_INTEGER TotalBytes;
    LARGE_INTEGER FreeBytes;
    TCHAR TotalSizeBuffer[10];
    YORI_STRING StrTotalSize;
    TCHAR FreeSizeBuffer[10];
    YORI_STRING StrFreeSize;
    DWORD PercentageUsed;
    BOOL Result = FALSE;
    WIN32_FIND_DATA FindData;
    YORI_STRING VtAttribute;
    TCHAR VtAttributeBuffer[YORI_MAX_INTERNAL_VT_ESCAPE_CHARS];

    TCHAR MountPointName[MAX_PATH];
    LPTSTR NameToReport;

    //
    //  Prepare string buffers to hold the human readable sizes.
    //

    YoriLibInitEmptyString(&StrTotalSize);
    StrTotalSize.StartOfString = TotalSizeBuffer;
    StrTotalSize.LengthAllocated = sizeof(TotalSizeBuffer)/sizeof(TotalSizeBuffer[0]);
    YoriLibInitEmptyString(&StrFreeSize);
    StrFreeSize.StartOfString = FreeSizeBuffer;
    StrFreeSize.LengthAllocated = sizeof(FreeSizeBuffer)/sizeof(FreeSizeBuffer[0]);
    YoriLibInitEmptyString(&VtAttribute);
    VtAttribute.StartOfString = VtAttributeBuffer;
    VtAttribute.LengthAllocated = sizeof(VtAttributeBuffer)/sizeof(VtAttributeBuffer[0]);

    //
    //  If the OS supports it, try to translate the GUID volume names back
    //  into drive letters.
    //

    NameToReport = VolName;
    if (DllKernel32.pGetVolumePathNamesForVolumeNameW) {
        if (DllKernel32.pGetVolumePathNamesForVolumeNameW(VolName, MountPointName, sizeof(MountPointName)/sizeof(MountPointName[0]), &PercentageUsed)) {
            NameToReport = MountPointName;
        }
    }

    //
    //  Query free space and display the result.
    //

    if (YoriLibGetDiskFreeSpace(VolName, &FreeBytes, &TotalBytes, NULL)) {

        YoriLibFileSizeToString(&StrTotalSize, &TotalBytes);
        YoriLibFileSizeToString(&StrFreeSize, &FreeBytes);

        if (DfContext->MinimalDisplay) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%lli %lli %s\n"), TotalBytes, FreeBytes, NameToReport, NameToReport);
        } 

        while (TotalBytes.HighPart != 0) {
            TotalBytes.QuadPart = TotalBytes.QuadPart >> 1;
            FreeBytes.QuadPart = FreeBytes.QuadPart >> 1;
        }
        PercentageUsed = 1000 - (DWORD)(FreeBytes.QuadPart * 1000 / TotalBytes.QuadPart);

        if (!DfContext->MinimalDisplay) {
            LPTSTR FinalComponent;
            YORILIB_COLOR_ATTRIBUTES Attribute;
            YORI_STRING YsVolName;

            if (DfContext->VolumesDisplayed > 0) {
                YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("\n"));
            }

            YoriLibUpdateFindDataFromFileInformation(&FindData, VolName, FALSE);
            FinalComponent = _tcsrchr(NameToReport, '\\');
            if (FinalComponent != NULL) {
                YoriLibSPrintfS(FindData.cFileName, MAX_PATH, _T("%s"), FinalComponent + 1);
            } else {
                YoriLibSPrintfS(FindData.cFileName, MAX_PATH, _T("%s"), NameToReport);
            }
            YoriLibConstantString(&YsVolName, VolName);
            if (!YoriLibFileFiltCheckColorMatch(&DfContext->ColorRules, &YsVolName, &FindData, &Attribute)) {
                Attribute.Ctrl = YORILIB_ATTRCTRL_WINDOW_BG | YORILIB_ATTRCTRL_WINDOW_FG;
                Attribute.Win32Attr = (UCHAR)YoriLibVtGetDefaultColor();
            }
            YoriLibVtStringForTextAttribute(&VtAttribute, Attribute.Ctrl, Attribute.Win32Attr);
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT,
                          _T("%y%y%c[0m total %y%y%c[0m free %3i.%i%% used %y%s%c[0m\n"),
                          &DfContext->FileSizeColorString,
                          &StrTotalSize,
                          27,
                          &DfContext->FileSizeColorString,
                          &StrFreeSize,
                          27,
                          PercentageUsed / 10, PercentageUsed % 10,
                          &VtAttribute,
                          NameToReport,
                          27);
        }

        if (DfContext->DisplayGraph) {
            YORI_STRING Subset;
            WORD Background;
            DWORD TotalBarSize;
            DWORD BarsSet;
            DWORD Index;
            UCHAR Ctrl;

            DfContext->LineBuffer.StartOfString[0] = ' ';
            DfContext->LineBuffer.StartOfString[1] = '[';

            YoriLibInitEmptyString(&Subset);
            Subset.StartOfString = &DfContext->LineBuffer.StartOfString[2];
            Subset.LengthAllocated = DfContext->LineBuffer.LengthAllocated - 2;

            Ctrl = YORILIB_ATTRCTRL_WINDOW_BG;
            Background = (USHORT)(YoriLibVtGetDefaultColor() & 0xF0);

            if (PercentageUsed <= 700) {
                YoriLibVtStringForTextAttribute(&Subset, Ctrl, (USHORT)(Background | FOREGROUND_GREEN | FOREGROUND_INTENSITY));
            } else if (PercentageUsed <= 850) {
                YoriLibVtStringForTextAttribute(&Subset, Ctrl, (USHORT)(Background | FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY));
            } else {
                YoriLibVtStringForTextAttribute(&Subset, Ctrl, (USHORT)(Background | FOREGROUND_RED | FOREGROUND_INTENSITY));
            }

            DfContext->LineBuffer.LengthInChars = 2 + Subset.LengthInChars;

            TotalBarSize = DfContext->ConsoleWidth - 4;
            BarsSet = TotalBarSize * PercentageUsed / 1000;

            for (Index = 0; Index < BarsSet; Index++) {
                DfContext->LineBuffer.StartOfString[Index + DfContext->LineBuffer.LengthInChars] = '#';
            }
            for (; Index < TotalBarSize; Index++) {
                DfContext->LineBuffer.StartOfString[Index + DfContext->LineBuffer.LengthInChars] = ' ';
            }
            DfContext->LineBuffer.LengthInChars += TotalBarSize;

            Subset.StartOfString = &DfContext->LineBuffer.StartOfString[DfContext->LineBuffer.LengthInChars];
            Subset.LengthAllocated = DfContext->LineBuffer.LengthAllocated - DfContext->LineBuffer.LengthInChars;

            Subset.LengthInChars = YoriLibSPrintf(Subset.StartOfString, _T("%c[0m]\n"), 27);
            DfContext->LineBuffer.LengthInChars += Subset.LengthInChars;

            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%y"), &DfContext->LineBuffer);

        }
        Result = TRUE;
        DfContext->VolumesDisplayed++;
    }

    return Result;
}

#ifdef YORI_BUILTIN
/**
 The main entrypoint for the df builtin command.
 */
#define ENTRYPOINT YoriCmd_YDF
#else
/**
 The main entrypoint for the df standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the df cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the child process on success, or failure if the child
         could not be launched.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    YORI_STRING Arg;
    HANDLE FindHandle;
    TCHAR VolName[512];
    DF_CONTEXT DfContext;
    YORI_STRING Combined;

    ZeroMemory(&DfContext, sizeof(DfContext));

    YoriLibGetWindowDimensions(GetStdHandle(STD_OUTPUT_HANDLE), &DfContext.ConsoleWidth, NULL);
    DfContext.DisplayGraph = TRUE;

    for (i = 1; i < ArgC; i++) {

        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));
        ArgumentUnderstood = FALSE;

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                DfHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2017-2019"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("m")) == 0) {
                DfContext.MinimalDisplay = TRUE;
                DfContext.DisplayGraph = FALSE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                StartArg = i;
                ArgumentUnderstood = TRUE;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    //
    //  Load the default color string and parse it into rules.
    //

    if (YoriLibLoadCombinedFileColorString(NULL, &Combined)) {
        YORI_STRING ErrorSubstring;
        YoriLibFileFiltParseColorString(&DfContext.ColorRules, &Combined, &ErrorSubstring);
        YoriLibFreeStringContents(&Combined);
    }

    YoriLibConstantString(&Combined, _T("fs"));
    if (!YoriLibGetMetadataColor(&Combined, &DfContext.FileSizeColor)) {
        DfContext.FileSizeColor.Ctrl = YORILIB_ATTRCTRL_WINDOW_BG | YORILIB_ATTRCTRL_WINDOW_FG;
        DfContext.FileSizeColor.Win32Attr = (UCHAR)YoriLibVtGetDefaultColor();
    }

    DfContext.FileSizeColorString.StartOfString = DfContext.FileSizeColorStringBuffer;
    DfContext.FileSizeColorString.LengthAllocated = YORI_MAX_INTERNAL_VT_ESCAPE_CHARS;

    YoriLibVtStringForTextAttribute(&DfContext.FileSizeColorString, DfContext.FileSizeColor.Ctrl, DfContext.FileSizeColor.Win32Attr);

    if (DfContext.DisplayGraph) {
        if (!YoriLibAllocateString(&DfContext.LineBuffer, DfContext.ConsoleWidth + 2 * YORI_MAX_INTERNAL_VT_ESCAPE_CHARS)) {
            YoriLibFileFiltFreeFilter(&DfContext.ColorRules);
            return EXIT_FAILURE;
        }
    }

    //
    //  Just return errors.  Don't display a dialog to indicate no disk is in
    //  a drive.
    //

    SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX);

    if (StartArg != 0) {
        for (i = StartArg; i < ArgC; i++) {
            if (!DfReportSingleVolume(ArgV[i].StartOfString, &DfContext)) {
                YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("df: Could not query %y\n"), &ArgV[i]);
            }
        }
    } else {
        FindHandle = YoriLibFindFirstVolume(VolName, sizeof(VolName)/sizeof(VolName[0]));
        if (FindHandle != INVALID_HANDLE_VALUE) {
            do {
                DfReportSingleVolume(VolName, &DfContext);
            } while(YoriLibFindNextVolume(FindHandle, VolName, sizeof(VolName)/sizeof(VolName[0])));
            YoriLibFindVolumeClose(FindHandle);
        }
    }

    YoriLibFileFiltFreeFilter(&DfContext.ColorRules);
    YoriLibFreeStringContents(&DfContext.LineBuffer);

    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>lib/recycle.c<|end_filename|>
/**
 * @file lib/recycle.c
 *
 * Yori shell send files to the recycle bin.
 *
 * Copyright (c) 2018 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS ERASE A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE ERASE ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>


/**
 Attempt to send an object to the recycle bin.

 @param FilePath Pointer to the file path to delete.

 @return TRUE if the object was sent to the recycle bin, FALSE if not.
 */
BOOL
YoriLibRecycleBinFile(
    __in PYORI_STRING FilePath
    )
{
    YORI_SHFILEOP FileOp;
    YORI_STRING FilePathWithDoubleNull;
    INT Result;

    YoriLibLoadShell32Functions();

    //
    //  If loading shell failed or we couldn't find the function, recycling
    //  won't happen.
    //

    if (DllShell32.pSHFileOperationW == NULL) {
        return FALSE;
    }

    //
    //  Create a double NULL terminated file name.
    //

    YoriLibInitEmptyString(&FilePathWithDoubleNull);
    if (!YoriLibAllocateString(&FilePathWithDoubleNull, FilePath->LengthInChars + 2)) {
        return FALSE;
    }

    //
    //  Shell will explode if it sees \\?\, so try to reconvert back to
    //  Win32 limited paths.
    //

    YoriLibUnescapePath(FilePath, &FilePathWithDoubleNull);

    ASSERT(FilePathWithDoubleNull.LengthAllocated >= FilePathWithDoubleNull.LengthInChars + 2);
    FilePathWithDoubleNull.StartOfString[FilePathWithDoubleNull.LengthInChars] = '\0';
    FilePathWithDoubleNull.StartOfString[FilePathWithDoubleNull.LengthInChars + 1] = '\0';

    //
    //  Ask shell to send the object to the recycle bin.
    //

    ZeroMemory(&FileOp, sizeof(FileOp));
    FileOp.Function = YORI_SHFILEOP_DELETE;
    FileOp.Source = FilePathWithDoubleNull.StartOfString;
    FileOp.Flags = YORI_SHFILEOP_FLAG_SILENT|YORI_SHFILEOP_FLAG_NOCONFIRMATION|YORI_SHFILEOP_FLAG_ALLOWUNDO|YORI_SHFILEOP_FLAG_NOERRORUI;

    Result = DllShell32.pSHFileOperationW(&FileOp);
    YoriLibFreeStringContents(&FilePathWithDoubleNull);

    if (Result == 0) {
        return TRUE;
    }
    return FALSE;
}

// vim:sw=4:ts=4:et:


<|start_filename|>yui/yui.c<|end_filename|>
/**
 * @file yui/yui.c
 *
 * Yori shell display lightweight graphical UI
 *
 * Copyright (c) 2019 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>
#include "yui.h"

/**
 Help text to display to the user.
 */
const
CHAR strYuiHelpText[] =
        "\n"
        "Display lightweight graphical UI.\n"
        "\n"
        "YUI\n";

/**
 Display usage text to the user.
 */
BOOL
YuiHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Yui %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strYuiHelpText);
    return TRUE;
}

/**
 The global application context.
 */
YUI_ENUM_CONTEXT YuiContext;

/**
 The height of the taskbar, in pixels.
 */
#define YUI_TASKBAR_HEIGHT (28)


/**
 Indicates that the screen resolution has changed and the taskbar needs to be
 repositioned accordingly.

 @param hWnd The taskbar window to reposition.

 @param ScreenWidth The new width of the screen.

 @param ScreenHeight The new height of the screen.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YuiNotifyResolutionChange(
    __in HWND hWnd,
    __in DWORD ScreenWidth,
    __in DWORD ScreenHeight
    )
{
    YORI_APPBARDATA AppBar;
    RECT ClientRect;

    if (YuiContext.DisplayResolutionChangeInProgress) {
        return TRUE;
    }

    YuiContext.DisplayResolutionChangeInProgress = TRUE;

    if (DllShell32.pSHAppBarMessage != NULL) {

        AppBar.cbSize = sizeof(AppBar);
        AppBar.hWnd = hWnd;
        AppBar.uCallbackMessage = WM_USER;
        AppBar.uEdge = 3;
        AppBar.rc.left = 0;
        AppBar.rc.top = ScreenHeight - YUI_TASKBAR_HEIGHT;
        AppBar.rc.bottom = ScreenHeight;
        AppBar.rc.right = ScreenWidth;
        AppBar.lParam = TRUE;

        // New
        DllShell32.pSHAppBarMessage(0, &AppBar);

        // Query (request) position
        DllShell32.pSHAppBarMessage(2, &AppBar);

        if (AppBar.rc.bottom - AppBar.rc.top < YUI_TASKBAR_HEIGHT) {
            AppBar.rc.top = AppBar.rc.bottom - YUI_TASKBAR_HEIGHT;
        }

        MoveWindow(hWnd,
                   AppBar.rc.left,
                   AppBar.rc.top,
                   AppBar.rc.right - AppBar.rc.left,
                   AppBar.rc.bottom - AppBar.rc.top,
                   TRUE);

        // Set position
        DllShell32.pSHAppBarMessage(3, &AppBar);

        // Activate
        DllShell32.pSHAppBarMessage(6, &AppBar);
    } else {
        MoveWindow(hWnd,
                   0,
                   ScreenHeight - YUI_TASKBAR_HEIGHT,
                   ScreenWidth,
                   YUI_TASKBAR_HEIGHT,
                   TRUE);
    }

    GetClientRect(hWnd, &ClientRect);

    if (YuiContext.hWndClock != NULL) {
        MoveWindow(YuiContext.hWndClock,
                   ClientRect.right - YUI_CLOCK_WIDTH - 1,
                   1,
                   YUI_CLOCK_WIDTH,
                   ClientRect.bottom - 2,
                   TRUE);
    }

    YuiTaskbarNotifyResolutionChange(&YuiContext);

    YuiContext.DisplayResolutionChangeInProgress = FALSE;

    return TRUE;
}


/**
 Server core systems do not accurately indicate changes including toplevel
 window creation, deletion, or activation.  On these systems Yui is forced
 to inefficiently poll.  Since the registration for notification succeeds,
 the only way to know whether polling is necessary is to inspect the
 product SKU and based on the result, infer whether the notification system
 is buggy or not.

 @return TRUE to indicate that the current system is a server core like
         system, and FALSE if it appears to be a full UI system.
 */
BOOL
YuiIsServerCore(VOID)
{
    DWORD ProductType;

    //
    //  If the API that indicates whether server core is present is not
    //  present, that implies we're not running on server core.
    //
    if (DllKernel32.pGetProductInfo == NULL) {
        return FALSE;
    }

    if (!DllKernel32.pGetProductInfo(6, 1, 0, 0, &ProductType)) {
        return FALSE;
    }

    switch(ProductType) {
        case PRODUCT_DATACENTER_SERVER_CORE:
        case PRODUCT_STANDARD_SERVER_CORE:
        case PRODUCT_ENTERPRISE_SERVER_CORE:
        case PRODUCT_WEB_SERVER_CORE:
        case PRODUCT_DATACENTER_SERVER_CORE_V:
        case PRODUCT_STANDARD_SERVER_CORE_V:
        case PRODUCT_ENTERPRISE_SERVER_CORE_V:
        case PRODUCT_HYPERV:
        case PRODUCT_STORAGE_EXPRESS_SERVER_CORE:
        case PRODUCT_STORAGE_STANDARD_SERVER_CORE:
        case PRODUCT_STORAGE_WORKGROUP_SERVER_CORE:
        case PRODUCT_STORAGE_ENTERPRISE_SERVER_CORE:
        case PRODUCT_STANDARD_SERVER_SOLUTIONS_CORE:
        case PRODUCT_SOLUTION_EMBEDDEDSERVER_CORE:
        case PRODUCT_SMALLBUSINESS_SERVER_PREMIUM_CORE:
        case PRODUCT_DATACENTER_A_SERVER_CORE:
        case PRODUCT_STANDARD_A_SERVER_CORE:
        case PRODUCT_DATACENTER_WS_SERVER_CORE:
        case PRODUCT_STANDARD_WS_SERVER_CORE:
        case PRODUCT_DATACENTER_EVALUATION_SERVER_CORE:
        case PRODUCT_STANDARD_EVALUATION_SERVER_CORE:
        case PRODUCT_AZURE_SERVER_CORE:
            return TRUE;
    }

    return FALSE;
}

/**
 Display the start menu and perform any action requested.

 @return TRUE to indicate the start menu was displayed successfully.
 */
BOOL
YuiDisplayMenu(VOID)
{
    if (YuiContext.MenuActive) {
        return TRUE;
    }
    YuiContext.MenuActive = TRUE;
    SendMessage(YuiContext.hWndStart, BM_SETSTATE, TRUE, 0);
    YuiMenuDisplayAndExecute(&YuiContext, YuiContext.hWnd);
    SendMessage(YuiContext.hWndStart, BM_SETSTATE, FALSE, 0);
    YuiContext.MenuActive = FALSE;
    return TRUE;
}

/**
 The main window procedure which processes messages sent to the taskbar
 window.

 @param hwnd The window handle.

 @param uMsg The message identifier.

 @param wParam The first parameter associated with the window message.

 @param lParam The second parameter associated with the window message.

 @return A value which depends on the type of message being processed.
 */
LRESULT CALLBACK
YuiWindowProc(
    __in HWND hwnd,
    __in UINT uMsg,
    __in WPARAM wParam,
    __in LPARAM lParam
    )
{

    switch(uMsg) {
        case WM_COMMAND:
            {
                WORD CtrlId;
                CtrlId = LOWORD(wParam);
                if (CtrlId == YUI_START_BUTTON) {
                    YuiDisplayMenu();
                } else {
                    ASSERT(CtrlId >= YUI_FIRST_TASKBAR_BUTTON);
                    YuiTaskbarSwitchToTask(&YuiContext, CtrlId);
                }
            }
            break;
        case WM_TIMER:
            switch(wParam) {
                case YUI_WINDOW_POLL_TIMER:
                    YuiTaskbarSyncWithCurrent(&YuiContext);
                    break;
                case YUI_CLOCK_TIMER:
                    YuiTaskbarUpdateClock(&YuiContext);
                    break;
            }
            return 0;
        case WM_CLOSE:
            PostQuitMessage(0);
            return 0;
        case WM_DISPLAYCHANGE:
            YuiNotifyResolutionChange(YuiContext.hWnd, LOWORD(lParam), HIWORD(lParam));
            break;
        default:
            if (uMsg == YuiContext.ShellHookMsg) {
                switch(wParam) {
                    case HSHELL_WINDOWACTIVATED:
                    case HSHELL_RUDEAPPACTIVATED:
                        YuiTaskbarNotifyActivateWindow(&YuiContext, (HWND)lParam);
                        break;
                    case HSHELL_WINDOWCREATED:
                        YuiTaskbarNotifyNewWindow(&YuiContext, (HWND)lParam);
                        break;
                    case HSHELL_WINDOWDESTROYED:
                        YuiTaskbarNotifyDestroyWindow(&YuiContext, (HWND)lParam);
                        break;
                    case HSHELL_REDRAW:
                        YuiTaskbarNotifyTitleChange(&YuiContext, (HWND)lParam);
                        break;
                    default:
                        break;
                }
            }
    }
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

/**
 A function signature for the YuiShook.dll version of RegisterShellHookWindow,
 which can be used if RegisterShellHookWindow is not present.
 */
typedef
BOOL WINAPI YUISHOOK_REGISTER_SHELL_HOOK_WINDOW(HWND);

/**
 A pointer to a function for the YuiShook.dll version of
 RegisterShellHookWindow, which can be used if RegisterShellHookWindow is not
 present.
 */
typedef YUISHOOK_REGISTER_SHELL_HOOK_WINDOW *PYUISHOOK_REGISTER_SHELL_HOOK_WINDOW;


/**
 Create the taskbar window, start button, and other assorted global elements,
 including populating the start menu and task bar with current state.

 @param Context Pointer to the application context.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
YuiCreateWindow(
    __in PYUI_ENUM_CONTEXT Context
    )
{
    WNDCLASS wc;
    BOOL Result;
    HDC hDC;
    RECT ClientRect;
    DWORD ScreenHeight;
    DWORD ScreenWidth;

    wc.style = 0;
    wc.lpfnWndProc = YuiWindowProc;
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hInstance = NULL;
    wc.hIcon = NULL;
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(DWORD_PTR)COLOR_WINDOW;
    wc.lpszMenuName = NULL;
    wc.lpszClassName = _T("YuiWnd");

    Result = RegisterClass(&wc);
    ASSERT(Result);


    ScreenHeight = GetSystemMetrics(SM_CYSCREEN);
    ScreenWidth = GetSystemMetrics(SM_CXSCREEN);

    Context->hWnd = CreateWindowEx(WS_EX_TOOLWINDOW | WS_EX_TOPMOST,
                                   _T("YuiWnd"),
                                   _T("Yui"),
                                   WS_POPUP | WS_DLGFRAME | WS_CLIPCHILDREN,
                                   0,
                                   ScreenHeight - YUI_TASKBAR_HEIGHT,
                                   ScreenWidth,
                                   YUI_TASKBAR_HEIGHT,
                                   NULL, NULL, NULL, 0);
    if (Context->hWnd == NULL) {
        return FALSE;
    }

    YuiNotifyResolutionChange(Context->hWnd, ScreenWidth, ScreenHeight);

    GetClientRect(Context->hWnd, &ClientRect);

    hDC = GetWindowDC(Context->hWnd);
    Context->hFont = CreateFont(-MulDiv(8, GetDeviceCaps(hDC, LOGPIXELSY), 72),
                                0,
                                0,
                                0,
                                FW_NORMAL,
                                FALSE,
                                FALSE,
                                FALSE,
                                DEFAULT_CHARSET,
                                OUT_DEFAULT_PRECIS,
                                CLIP_DEFAULT_PRECIS,
                                DEFAULT_QUALITY,
                                FF_DONTCARE,
                                _T("Tahoma"));
    ReleaseDC(Context->hWnd, hDC);

    if (Context->hFont == NULL) {
        DestroyWindow(Context->hWnd);
        Context->hWnd = NULL;
        return FALSE;
    }

    Context->hWndStart = CreateWindow(_T("BUTTON"),
                                      _T("Start"),
                                      BS_PUSHBUTTON | BS_CENTER | WS_VISIBLE | WS_CHILD,
                                      1,
                                      1,
                                      YUI_START_BUTTON_WIDTH,
                                      ClientRect.bottom - 2,
                                      Context->hWnd,
                                      (HMENU)(DWORD_PTR)YUI_START_BUTTON,
                                      NULL,
                                      NULL);

    if (Context->hWndStart == NULL) {
        DeleteObject(Context->hFont);
        Context->hFont = NULL;
        DestroyWindow(Context->hWnd);
        Context->hWnd = NULL;
        return FALSE;
    }

    SendMessage(Context->hWndStart, WM_SETFONT, (WPARAM)Context->hFont, MAKELPARAM(TRUE, 0));

    YoriLibInitEmptyString(&Context->ClockDisplayedValue);
    Context->ClockDisplayedValue.StartOfString = Context->ClockDisplayedValueBuffer;
    Context->ClockDisplayedValue.LengthAllocated = sizeof(Context->ClockDisplayedValueBuffer)/sizeof(Context->ClockDisplayedValueBuffer[0]);

    Context->hWndClock = CreateWindowEx(WS_EX_STATICEDGE,
                                        _T("STATIC"),
                                        _T(""),
                                        SS_CENTER | SS_SUNKEN | WS_VISIBLE | WS_CHILD,
                                        ClientRect.right - YUI_CLOCK_WIDTH - 1,
                                        1,
                                        YUI_CLOCK_WIDTH,
                                        ClientRect.bottom - 2,
                                        Context->hWnd,
                                        NULL,
                                        NULL,
                                        NULL);

    if (Context->hWndClock == NULL) {
        DestroyWindow(Context->hWndStart);
        Context->hWndStart = NULL;
        DeleteObject(Context->hFont);
        Context->hFont = NULL;
        DestroyWindow(Context->hWnd);
        Context->hWnd = NULL;
        return FALSE;
    }

    SendMessage(Context->hWndClock, WM_SETFONT, (WPARAM)Context->hFont, MAKELPARAM(TRUE, 0));
    YuiTaskbarUpdateClock(Context);
    Context->ClockTimerId = SetTimer(Context->hWnd, YUI_CLOCK_TIMER, 5000, NULL);


    Context->LeftmostTaskbarOffset = 1 + YUI_START_BUTTON_WIDTH + 1;
    Context->RightmostTaskbarOffset = 1 + YUI_CLOCK_WIDTH + 1;


    YuiTaskbarPopulateWindows(Context, Context->hWnd);

    //
    //  Check if we're running on a platform that doesn't support
    //  notifications and we need to poll instead.
    //

    if (Context->TaskbarRefreshFrequency == 0 &&
        (DllUser32.pRegisterShellHookWindow == NULL || YuiIsServerCore())) {

        Context->TaskbarRefreshFrequency = 250;
    }

    //
    //  If we support notifications, attempt to set them up.
    //

    if (Context->TaskbarRefreshFrequency == 0) {
        Context->ShellHookMsg = RegisterWindowMessage(_T("SHELLHOOK"));
        if (!DllUser32.pRegisterShellHookWindow(Context->hWnd)) {
            Context->TaskbarRefreshFrequency = 250;
        }

    }

    //
    //  If the refresh frequency is specified, or the OS doesn't support
    //  notifications, or setting up notifications failed, set up polling
    //  now.
    //

    if (Context->TaskbarRefreshFrequency != 0) {
        Context->SyncTimerId = SetTimer(Context->hWnd, YUI_WINDOW_POLL_TIMER, Context->TaskbarRefreshFrequency, NULL);
    }

    ShowWindow(Context->hWnd, SW_SHOW);

    /*
    {
        HMODULE Helper;
        Context->ShellHookMsg = RegisterWindowMessage(_T("SHELLHOOK"));
        YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("ShellHookMsg %08x\n"), Context->ShellHookMsg);
        Helper = LoadLibrary(_T("YUISHOOK.DLL"));
        if (Helper != NULL) {
            PYUISHOOK_REGISTER_SHELL_HOOK_WINDOW pYuiShookRegisterShellHookWindow;
            pYuiShookRegisterShellHookWindow = (PYUISHOOK_REGISTER_SHELL_HOOK_WINDOW)GetProcAddress(Helper, "YuiShookRegisterShellHookWindow");
            if (pYuiShookRegisterShellHookWindow != NULL) {
                if (!pYuiShookRegisterShellHookWindow(Context->hWnd)) {
                    YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("RegisterShellHookWindow failed %i\n"), GetLastError());
                }
            } else {
                YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("RegisterShellHookWindow not exported %i\n"), GetLastError());
            }
        } else {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("YuiShook.dll not loaded %i\n"), GetLastError());
        }
    }
    */

    return TRUE;
}

/**
 Close any windows, timers, and other system resources.  In particular, note
 this tells explorer that the app bar is no longer present, which is something
 explorer really wants to be told because it has no process destruction
 notification.
 */ 
VOID
YuiCleanupGlobalState(VOID)
{
    DWORD Count;
    YuiMenuFreeAll(&YuiContext);
    YuiTaskbarFreeButtons(&YuiContext);

    for (Count = 0; Count < sizeof(YuiContext.StartChangeNotifications)/sizeof(YuiContext.StartChangeNotifications[0]); Count++) {
        if (YuiContext.StartChangeNotifications[Count] != NULL) {
            FindCloseChangeNotification(YuiContext.StartChangeNotifications[Count]);
            YuiContext.StartChangeNotifications[Count] = NULL;
        }
    }

    if (YuiContext.ClockTimerId != 0) {
        KillTimer(YuiContext.hWnd, YUI_CLOCK_TIMER);
        YuiContext.ClockTimerId = 0;
    }

    if (YuiContext.SyncTimerId != 0) {
        KillTimer(YuiContext.hWnd, YUI_WINDOW_POLL_TIMER);
        YuiContext.SyncTimerId = 0;
    }

    if (YuiContext.hWndClock != NULL) {
        DestroyWindow(YuiContext.hWndClock);
        YuiContext.hWndClock = NULL;
    }

    if (YuiContext.hWndStart != NULL) {
        DestroyWindow(YuiContext.hWndStart);
        YuiContext.hWndStart = NULL;
    }

    if (DllShell32.pSHAppBarMessage != NULL && YuiContext.hWnd != NULL) {
        YORI_APPBARDATA AppBar;

        AppBar.cbSize = sizeof(AppBar);
        AppBar.hWnd = YuiContext.hWnd;
        AppBar.uCallbackMessage = WM_USER;
        AppBar.uEdge = 3;
        AppBar.rc.left = 0;
        AppBar.rc.top = 0;
        AppBar.rc.bottom = 0;
        AppBar.rc.right = 0;
        AppBar.lParam = TRUE;

        // Remove
        DllShell32.pSHAppBarMessage(1, &AppBar);
    }

    if (YuiContext.hWnd != NULL) {
        DestroyWindow(YuiContext.hWnd);
        YuiContext.hWnd = NULL;
    }

    if (YuiContext.hFont) {
        DeleteObject(YuiContext.hFont);
        YuiContext.hFont = NULL;
    }
}



/**
 The main entrypoint for the yui cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process, zero on success, nonzero on failure.
 */
DWORD
ymain(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    YORI_STRING Arg;
    CONSOLE_SCREEN_BUFFER_INFO ScreenInfo;

    ZeroMemory(&YuiContext, sizeof(YuiContext));
    YoriLibInitializeListHead(&YuiContext.ProgramsDirectory.ListEntry);
    YoriLibInitializeListHead(&YuiContext.ProgramsDirectory.ChildDirectories);
    YoriLibInitializeListHead(&YuiContext.ProgramsDirectory.ChildFiles);
    YoriLibInitEmptyString(&YuiContext.ProgramsDirectory.DirName);
    YoriLibInitializeListHead(&YuiContext.StartDirectory.ListEntry);
    YoriLibInitializeListHead(&YuiContext.StartDirectory.ChildDirectories);
    YoriLibInitializeListHead(&YuiContext.StartDirectory.ChildFiles);
    YoriLibInitEmptyString(&YuiContext.StartDirectory.DirName);
    YoriLibInitializeListHead(&YuiContext.TaskbarButtons);
    YuiContext.TaskbarButtonCount = 0;

    YoriLibLoadUser32Functions();
    YoriLibLoadShell32Functions();
    YoriLibLoadWtsApi32Functions();

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                YuiHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2019"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("p")) == 0) {
                if (ArgC > i + 1) {
                    LONGLONG llTemp;
                    DWORD CharsConsumed;
                    if (YoriLibStringToNumber(&ArgV[i + 1], TRUE, &llTemp, &CharsConsumed) && CharsConsumed > 0) {
                        ArgumentUnderstood = TRUE;
                        YuiContext.TaskbarRefreshFrequency = (DWORD)llTemp;
                        i++;
                    }
                }
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    if (GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &ScreenInfo)) {
        if (ScreenInfo.dwCursorPosition.X == 0 && ScreenInfo.dwCursorPosition.Y == 0) {
            FreeConsole();
        }
    }

    YuiMenuPopulate(&YuiContext);
    if (!YuiCreateWindow(&YuiContext)) {
        return EXIT_FAILURE;
    }

    {
        MSG msg;
        while (GetMessage(&msg, NULL, 0, 0)) {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }

    YuiCleanupGlobalState();

    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>lib/movefile.c<|end_filename|>
/**
 * @file lib/movefile.c
 *
 * Yori shell move or rename a file
 *
 * Copyright (c) 2017-2020 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, MOVEESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 Rename or move a file or directory.  This routine will copy and replace
 files as needed to complete the request.

 @param Source Pointer to the source of the rename.

 @param FullDest Pointer to the target of the rename.

 @param ReplaceExisting If TRUE, any existing file is overwritten.  If FALSE,
        existing files are retained and the call fails with last error set
        appropriately.

 @param PosixSemantics If TRUE, the rename should apply POSIX semantics to
        remove an in use file from the namespace immediately.  If FALSE, Win32
        semantics are applied.

 @return Win32 error code, ERROR_SUCCESS to indicate success, or appropriate
         Win32 error.
 */
DWORD
YoriLibMoveFile(
    __in PYORI_STRING Source,
    __in PYORI_STRING FullDest,
    __in BOOLEAN ReplaceExisting,
    __in BOOLEAN PosixSemantics
    )
{
    DWORD OsMajor;
    DWORD OsMinor;
    DWORD OsBuild;
    DWORD Flags;
    DWORD Error;
    DWORD Attributes;
    DWORD NewAttributes;

    ASSERT(YoriLibIsStringNullTerminated(Source));
    ASSERT(YoriLibIsStringNullTerminated(FullDest));

    if (PosixSemantics) {
        PYORI_FILE_RENAME_INFO RenameInfo;
        DWORD RenameInfoSize;
        HANDLE hFile;

        if (DllKernel32.pSetFileInformationByHandle == NULL) {
            return ERROR_PROC_NOT_FOUND;
        }

        hFile = CreateFile(Source->StartOfString,
                           DELETE,
                           FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                           NULL,
                           OPEN_EXISTING,
                           FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
                           NULL);
        if (hFile == INVALID_HANDLE_VALUE) {
            return GetLastError();
        }

        //
        //  The string requires NULL termination and the structure includes
        //  one character, so allocate the structure plus the length of the
        //  string.
        //

        RenameInfoSize = sizeof(YORI_FILE_RENAME_INFO) + FullDest->LengthInChars * sizeof(TCHAR);
        RenameInfo = YoriLibMalloc(RenameInfoSize);
        if (RenameInfo == NULL) {
            CloseHandle(hFile);
            return ERROR_NOT_ENOUGH_MEMORY;
        }

        RenameInfo->Flags = FILE_RENAME_FLAG_POSIX_SEMANTICS;
        if (ReplaceExisting) {
            RenameInfo->Flags = RenameInfo->Flags | FILE_RENAME_FLAG_REPLACE_IF_EXISTS;
        }

        RenameInfo->RootDirectory = NULL;
        RenameInfo->FileNameLength = FullDest->LengthInChars * sizeof(TCHAR);
        memcpy(RenameInfo->FileName, FullDest->StartOfString, FullDest->LengthInChars * sizeof(TCHAR));
        RenameInfo->FileName[FullDest->LengthInChars] = '\0';

        Error = ERROR_SUCCESS;
        if (!DllKernel32.pSetFileInformationByHandle(hFile, FileRenameInfoEx, RenameInfo, RenameInfoSize)) {
            Error = GetLastError();
            if (ReplaceExisting && Error == ERROR_ACCESS_DENIED) {
                Attributes = GetFileAttributes(FullDest->StartOfString);
                if (Attributes != (DWORD)-1 &&
                    (Attributes & (FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM)) != 0) {

                    NewAttributes = Attributes & ~(FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM);
                    if (SetFileAttributes(FullDest->StartOfString, NewAttributes)) {
                        if (!DllKernel32.pSetFileInformationByHandle(hFile, FileRenameInfoEx, RenameInfo, RenameInfoSize)) {
                            SetFileAttributes(FullDest->StartOfString, Attributes);
                        } else {
                            Error = ERROR_SUCCESS;
                        }
                    }
                }
            }
        }

        CloseHandle(hFile);
        YoriLibFree(RenameInfo);

        //
        //  If the rename would cross volumes, this must be implemented by
        //  copy/delete, which SetFileInformationByHandle doesn't implement,
        //  so give up on POSIX and try boring Win32.
        //

        if (Error != ERROR_NOT_SAME_DEVICE) {
            return Error;
        }
    }

    Flags = MOVEFILE_COPY_ALLOWED;
    if (ReplaceExisting) {
        Flags = Flags | MOVEFILE_REPLACE_EXISTING;
    }

    if (!MoveFileEx(Source->StartOfString, FullDest->StartOfString, Flags)) {
        Error = GetLastError();
        if (ReplaceExisting && Error == ERROR_ACCESS_DENIED) {
            Attributes = GetFileAttributes(FullDest->StartOfString);
            if (Attributes != (DWORD)-1 &&
                (Attributes & (FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM)) != 0) {

                NewAttributes = Attributes & ~(FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM);
                if (!SetFileAttributes(FullDest->StartOfString, NewAttributes)) {
                    return Error;
                }

                if (!MoveFileEx(Source->StartOfString, FullDest->StartOfString, Flags)) {
                    SetFileAttributes(FullDest->StartOfString, Attributes);
                    return Error;
                }
                Error = ERROR_SUCCESS;
            }
        }

        if (Error != ERROR_SUCCESS) {
            return Error;
        }
    }

    YoriLibGetOsVersion(&OsMajor, &OsMinor, &OsBuild);

    //
    //  Windows 2000 and above claim to support inherited ACLs, except they
    //  really depend on applications to perform the inheritance.  On these
    //  systems, attempt to reset the ACL on the target, which really resets
    //  security and picks up state from the parent.  Don't do this on older
    //  systems, because that will result in a file with no ACL.  Note this
    //  can fail due to not having access on the file, or a file system
    //  that doesn't support security, or because the file is no longer there,
    //  so errors here are just ignored.  Nano is missing most of AdvApi32,
    //  so this code currently doesn't run there.
    //

    if (OsMajor >= 5 &&
        DllAdvApi32.pSetNamedSecurityInfoW != NULL &&
        DllAdvApi32.pInitializeAcl != NULL) {

        ACL EmptyAcl;

        memset(&EmptyAcl, 0, sizeof(EmptyAcl));

        DllAdvApi32.pInitializeAcl(&EmptyAcl, sizeof(EmptyAcl), ACL_REVISION);
        DllAdvApi32.pSetNamedSecurityInfoW(FullDest->StartOfString, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION | UNPROTECTED_DACL_SECURITY_INFORMATION, NULL, NULL, &EmptyAcl, NULL);
    }

    return ERROR_SUCCESS;
}

/**
 Call CopyFile, and if the operation fails, check if it's due to readonly,
 hidden or system attributes on the target, clear those and retry.

 @param SourceFile The source file name, expected to be NULL terminated.

 @param DestFile The destination file name, expected to be NULL terminated.

 @return The Win32 error code, possibly ERROR_SUCCESS or appropriate error
         on failure.
 */
DWORD
YoriLibCopyFile(
    __in PYORI_STRING SourceFile,
    __in PYORI_STRING DestFile
    )
{
    DWORD Error;
    DWORD Attributes;
    DWORD NewAttributes;

    ASSERT(YoriLibIsStringNullTerminated(SourceFile));
    ASSERT(YoriLibIsStringNullTerminated(DestFile));

    Error = ERROR_SUCCESS;
    if (!CopyFile(SourceFile->StartOfString, DestFile->StartOfString, FALSE)) {
        Error = GetLastError();
        if (Error == ERROR_ACCESS_DENIED) {
            Attributes = GetFileAttributes(DestFile->StartOfString);
            if (Attributes != (DWORD)-1 &&
                (Attributes & (FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM)) != 0) {

                NewAttributes = Attributes & ~(FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM);
                if (!SetFileAttributes(DestFile->StartOfString, NewAttributes)) {
                    return Error;
                }

                if (!CopyFile(SourceFile->StartOfString, DestFile->StartOfString, FALSE)) {
                    SetFileAttributes(DestFile->StartOfString, Attributes);
                    return Error;
                }
                Error = ERROR_SUCCESS;
            }
        }
    }

    return Error;
}


// vim:sw=4:ts=4:et:


<|start_filename|>make/exec.c<|end_filename|>
/**
 * @file make/exec.c
 *
 * Yori shell make execute child process support
 *
 * Copyright (c) 2020 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>
#include <yorish.h>
#include "make.h"

/**
 Information about a currently executing child process.
 */
typedef struct _MAKE_CHILD_RECIPE {

    /**
     Set to TRUE to indicate that the command for the child has been parsed
     and on completion this parsed state needs to be torn down.  If FALSE,
     a process has not launched successfully so this structure can be used
     without tearing anything down.
     */
    BOOLEAN CmdContextPresent;

    /**
     Indicates the job identifier.
     */
    DWORD JobId;

    /**
     The current directory for this recipe.
     */
    YORI_STRING CurrentDirectory;

    /**
     The target that has requested this child process to be performed.
     */
    PMAKE_TARGET Target;

    /**
     The current command within the target being executed.
     */
    PMAKE_CMD_TO_EXEC Cmd;

    /**
     A handle to a child process to wait for completion.  This is the same
     value as embedded in the ExecPlan and is replicated here only to make
     code referring to it shorter.
     */
    HANDLE ProcessHandle;

    /**
     A command context.  Should be deallocated if CmdContextPresent is TRUE.
     */
    YORI_LIBSH_CMD_CONTEXT CmdContext;

    /**
     An execution plan.  Should be deallocated if CmdContextPresent is TRUE.
     */
    YORI_LIBSH_EXEC_PLAN ExecPlan;
} MAKE_CHILD_RECIPE, *PMAKE_CHILD_RECIPE;

/**
 Attempt to set the temporary directory for this process to match the
 specified JobId, creating the directory if it does not exist.

 @param MakeContext Pointer to the make context, indicating which directories
        have been created and what the parent temporary path is.

 @param JobId Specifies the job identifier.

 @return TRUE to indicate the directory exists and has been set to be the
         active temporary directory, or FALSE if the directory could not be
         changed.
 */
BOOL
MakeSetTemporaryDirectory(
    __in PMAKE_CONTEXT MakeContext,
    __in DWORD JobId
    )
{
    DWORDLONG TestMask;
    YORI_STRING JobTempPath;

    ASSERT(JobId < MakeContext->NumberProcesses);
    TestMask = 1;
    TestMask = TestMask << JobId;

    if (!YoriLibAllocateString(&JobTempPath, MakeContext->TempPath.LengthInChars + sizeof("\\YMAKE12"))) {
        return FALSE;
    }

    JobTempPath.LengthInChars = YoriLibSPrintf(JobTempPath.StartOfString, _T("%y\\YMAKE%i"), &MakeContext->TempPath, JobId);

    if ((MakeContext->TempDirectoriesCreated & TestMask) == 0) {
        if (!YoriLibCreateDirectoryAndParents(&JobTempPath)) {
            YoriLibFreeStringContents(&JobTempPath);
            return FALSE;
        }

        MakeContext->TempDirectoriesCreated = MakeContext->TempDirectoriesCreated | TestMask;
    }

    if (!SetEnvironmentVariable(_T("TEMP"), JobTempPath.StartOfString)) {
        YoriLibFreeStringContents(&JobTempPath);
        return FALSE;
    }

    if (!SetEnvironmentVariable(_T("TMP"), JobTempPath.StartOfString)) {
        YoriLibFreeStringContents(&JobTempPath);
        return FALSE;
    }

    YoriLibFreeStringContents(&JobTempPath);
    return TRUE;
}

/**
 Attempt to remove any temporary directories created by this program.
 Currently directories that contain files are not removed.  This occurs if a
 child process did not clean up temporary files.

 @param MakeContext Pointer to the make context.
 */
VOID
MakeCleanupTemporaryDirectories(
    __in PMAKE_CONTEXT MakeContext
    )
{
    DWORD Probe;
    DWORDLONG TestMask;
    YORI_STRING TempPath;

    if (!YoriLibAllocateString(&TempPath, MakeContext->TempPath.LengthInChars + sizeof("\\YMAKE12"))) {
        return;
    }

    for (Probe = 0; Probe < MakeContext->NumberProcesses; Probe++) {
        TestMask = 1;
        TestMask = TestMask << Probe;
        if ((MakeContext->TempDirectoriesCreated & TestMask) != 0) {
            TempPath.LengthInChars = YoriLibSPrintf(TempPath.StartOfString, _T("%y\\YMAKE%i"), &MakeContext->TempPath, Probe);
            RemoveDirectory(TempPath.StartOfString);
        }
    }

    YoriLibFreeStringContents(&TempPath);
}

/**
 Allocate a job identifier for the child process.  This is a number between
 zero to the maximum number of child processes minus one.  Because of the
 limit in the number of child processes, one of these values should always
 be available.

 @param MakeContext Pointer to the make context.

 @return The job identifier.
 */
DWORD
MakeAllocateJobId(
    __in PMAKE_CONTEXT MakeContext
    )
{
    DWORD Probe;
    DWORDLONG TestMask;

    for (Probe = 0; Probe < MakeContext->NumberProcesses; Probe++) {
        TestMask = 1;
        TestMask = TestMask << Probe;
        if ((MakeContext->JobIdsAllocated & TestMask) == 0) {
            MakeContext->JobIdsAllocated = MakeContext->JobIdsAllocated | TestMask;
            MakeSetTemporaryDirectory(MakeContext, Probe);
            return Probe;
        }
    }

    ASSERT(Probe < MakeContext->NumberProcesses);
    return Probe;
}

/**
 Free a job identifier that was previously associated with a child process.

 @param MakeContext Pointer to the make context.

 @param JobId The job identifier.
 */
VOID
MakeFreeJobId(
    __in PMAKE_CONTEXT MakeContext,
    __in DWORD JobId
    )
{
    DWORDLONG TestMask;

    ASSERT(JobId < MakeContext->NumberProcesses);

    TestMask = 1;
    TestMask = TestMask << JobId;
    ASSERT(MakeContext->JobIdsAllocated & TestMask);
    MakeContext->JobIdsAllocated = MakeContext->JobIdsAllocated & ~(TestMask);
}

/**
 The list of commands to invoke via cmd /c without trying to spawn an
 external process.
 */
CONST LPTSTR
MakePuntToCmd[] = {
    _T("COPY"),
    _T("ERASE"),
    _T("FOR"),
    _T("IF"),
    _T("MOVE"),
    _T("REN"),
    _T("TYPE")
};

/**
 Return TRUE if there are more commands to execute as part of constructing
 this target, or FALSE if this target is complete.

 @param ChildRecipe Pointer to the child recipe structure specifying the
        target.

 @return TRUE if there are more commands to execute as part of constructing
         this target, or FALSE if this target is complete.
 */
BOOLEAN
MakeDoesTargetHaveMoreCommands(
    __in PMAKE_CHILD_RECIPE ChildRecipe
    )
{
    PYORI_LIST_ENTRY ListEntry;
    ListEntry = YoriLibGetNextListEntry(&ChildRecipe->Target->ExecCmds, &ChildRecipe->Cmd->ListEntry);
    if (ListEntry == NULL) {
        return FALSE;
    }

    return TRUE;
}

/**
 Change the current directory for the recipe.

 Unlike a "real" change current directory, this is not modifying global
 process state.  It can evaluate relative paths to the current recipe path,
 which is common, but it cannot evaluate things relative to other drive
 letters without recording a full per-drive set of current directories for
 each recipe.  This seems unnecessary because build systems want to traverse
 directories within their build tree, and changing drives would be outside
 that.  If changing drives were supported, we'd also need to implement the
 x: alias notation.

 @param ChildRecipe Pointer to the child recipe specifying the current
        directory for the recipe.

 @param ArgC Specifies the number of arguments.

 @param ArgV Pointer to an array of arguments.

 @return EXIT_FAILURE or EXIT_SUCCESS as appropriate.
 */
DWORD
MakeProcessCd(
    __in PMAKE_CHILD_RECIPE ChildRecipe,
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    YORI_STRING NewCurrentDirectory;

    if (ArgC < 2) {
        return EXIT_FAILURE;
    }

    YoriLibInitEmptyString(&NewCurrentDirectory);

    if (!YoriLibGetFullPathNameRelativeTo(&ChildRecipe->CurrentDirectory, &ArgV[1], FALSE, &NewCurrentDirectory, NULL)) {
        return EXIT_FAILURE;
    }

    YoriLibFreeStringContents(&ChildRecipe->CurrentDirectory);
    memcpy(&ChildRecipe->CurrentDirectory, &NewCurrentDirectory, sizeof(YORI_STRING));
    return EXIT_SUCCESS;
}

/**
 Inspect an if statement and indicate whether it can be handled in process or
 needs to be handled by CMD.  Currently in process support only exists for
 "if exist" or "if not exist".  If support exists in process, the command to
 execute is returned in CmdToExec.  Note that the command to execute may be
 an empty string, indicating the condition is not satisfied, and no command
 needs to be executed.

 @param ChildRecipe Pointer to information about the child recipe to
        execute, including the working directory.

 @param ArgC Specifies the number of arguments.

 @param ArgV Specifies an array of arguments.

 @param CmdToExec On input, specifies the full command string of the if
        expression.  On output, if the command can be handled in proc, this
        is updated to contain the command to execute in CMD, which may be
        empty.

 @return TRUE to indicate this if expression was handled in proc, and the
         CmdToExec string is updated to indicate the external command to
         invoke.  FALSE to indicate the expression should be resolved by CMD.
 */
__success(return)
BOOLEAN
MakeProcessIf(
    __in PMAKE_CHILD_RECIPE ChildRecipe,
    __in DWORD ArgC,
    __in YORI_STRING ArgV[],
    __inout PYORI_STRING CmdToExec
    )
{
    DWORD Index;
    BOOLEAN Not;
    YORI_STRING FullPath;
    HANDLE FindHandle;
    WIN32_FIND_DATA FindData;
    BOOLEAN Found;
    BOOLEAN ConditionTrue;

    Not = FALSE;

    for (Index = 1; Index < ArgC; Index++) {
        if (YoriLibCompareStringWithLiteralInsensitive(&ArgV[Index], _T("NOT")) == 0) {
            if (Not) {
                Not = FALSE;
            } else {
                Not = TRUE;
            }
        } else if (YoriLibCompareStringWithLiteralInsensitive(&ArgV[Index], _T("EXIST")) == 0 &&
                   Index + 1 < ArgC) {
            break;
        } else {
            return FALSE;
        }
    }

    if (Index + 1 > ArgC) {
        return FALSE;
    }

    YoriLibInitEmptyString(&FullPath);
    if (!YoriLibGetFullPathNameRelativeTo(&ChildRecipe->CurrentDirectory, &ArgV[Index + 1], TRUE, &FullPath, NULL)) {
        return FALSE;
    }

    Found = FALSE;
    FindHandle = FindFirstFile(FullPath.StartOfString, &FindData);
    if (FindHandle != INVALID_HANDLE_VALUE) {
        Found = TRUE;
        FindClose(FindHandle);
    }

    YoriLibFreeStringContents(&FullPath);

    ConditionTrue = FALSE;
    if ((Found && !Not) || (!Found && Not)) {

        ConditionTrue = TRUE;
    }

    if (ArgC > Index + 2 && ConditionTrue) {
        if (!YoriLibBuildCmdlineFromArgcArgv(ArgC - Index - 2, &ArgV[Index + 2], TRUE, TRUE, &FullPath)) {
            return FALSE;
        }
    }

    YoriLibFreeStringContents(CmdToExec);
    memcpy(CmdToExec, &FullPath, sizeof(YORI_STRING));
    return TRUE;
}


/**
 If there is a CmdContext or ExecPlan attached to the child process structure,
 free it now in preparation for reuse.

 @param ChildRecipe Pointer to the child recipe structure.
 */
VOID
MakeFreeCmdContextIfNecessary(
    __in PMAKE_CHILD_RECIPE ChildRecipe
    )
{
    if (ChildRecipe->CmdContextPresent) {
        YoriLibShFreeExecPlan(&ChildRecipe->ExecPlan);
        YoriLibShFreeCmdContext(&ChildRecipe->CmdContext);
        ChildRecipe->CmdContextPresent = FALSE;
    }
}

/**
 Start executing the next command within a target.

 @param MakeContext Pointer to the make context.

 @param ChildRecipe Pointer to the child recipe structure specifying the
        target.

 @return TRUE to indicate that a child process was successfully created, or
         FALSE if it was not.  Note that if the command indicates that failure
         is tolerable, this function can return TRUE without creating a 
         child process, which is indicated by the ChildRecipe->ProcessHandle
         member being NULL.
 */
BOOLEAN
MakeLaunchNextCmd(
    __in PMAKE_CONTEXT MakeContext,
    __inout PMAKE_CHILD_RECIPE ChildRecipe
    )
{
    STARTUPINFO si;
    PYORI_LIST_ENTRY ListEntry;
    PMAKE_TARGET Target;
    PMAKE_CMD_TO_EXEC CmdToExec;
    PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext;
    DWORD Index;
    DWORD Error;
    BOOL FailedInRedirection;
    YORI_STRING CmdToParse;
    BOOLEAN ExecutedBuiltin;
    BOOLEAN PuntToCmd;
    BOOLEAN Reparse;

    ASSERT(!ChildRecipe->CmdContextPresent);

    Target = ChildRecipe->Target;

    //
    //  Get the next command within the recipe.
    //

    if (ChildRecipe->Cmd == NULL) {
        ListEntry = YoriLibGetNextListEntry(&Target->ExecCmds, NULL);
    } else {
        ListEntry = YoriLibGetNextListEntry(&Target->ExecCmds, &ChildRecipe->Cmd->ListEntry);
    }

    //
    //  Each target being executed should have at least one command, and when
    //  it finishes, we need to check whether to call here or update
    //  dependencies and move to the next target
    //

    ASSERT(ListEntry != NULL);
    ChildRecipe->Cmd = CONTAINING_RECORD(ListEntry, MAKE_CMD_TO_EXEC, ListEntry);

    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);

    CmdToExec = CONTAINING_RECORD(ListEntry, MAKE_CMD_TO_EXEC, ListEntry);
    if (CmdToExec->DisplayCmd && !MakeContext->SilentCommandLaunching) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%y\n"), &CmdToExec->Cmd);
    }


    YoriLibInitEmptyString(&CmdToParse);
    CmdToParse.StartOfString = CmdToExec->Cmd.StartOfString;
    CmdToParse.LengthInChars = CmdToExec->Cmd.LengthInChars;

    //
    //  Check if this command is a builtin, and if so, execute it inline
    //

    PuntToCmd = FALSE;
    while(TRUE) {
        ExecutedBuiltin = FALSE;
        PuntToCmd = FALSE;
        Reparse = FALSE;

        ASSERT(!ChildRecipe->CmdContextPresent);

        if (!YoriLibShParseCmdlineToCmdContext(&CmdToParse, 0, &ChildRecipe->CmdContext)) {
            YoriLibFreeStringContents(&CmdToParse);
            return FALSE;
        }

        if (!YoriLibShParseCmdContextToExecPlan(&ChildRecipe->CmdContext, &ChildRecipe->ExecPlan, NULL, NULL, NULL, NULL)) {
            YoriLibShFreeCmdContext(&ChildRecipe->CmdContext);
            YoriLibFreeStringContents(&CmdToParse);
            return FALSE;
        }

        ChildRecipe->CmdContextPresent = TRUE;
        ExecContext = ChildRecipe->ExecPlan.FirstCmd;

        if (ExecContext->CmdToExec.ArgV == NULL ||
            ExecContext->CmdToExec.ArgC == 0) {

            ExecutedBuiltin = TRUE;
            ChildRecipe->ProcessHandle = NULL;

        } else {

            DWORD Result = EXIT_SUCCESS;
            if (YoriLibCompareStringWithLiteralInsensitive(&ExecContext->CmdToExec.ArgV[0], _T("IF")) == 0) {
                if (MakeProcessIf(ChildRecipe, ExecContext->CmdToExec.ArgC, ExecContext->CmdToExec.ArgV, &CmdToParse)) {
                    Reparse = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&ExecContext->CmdToExec.ArgV[0], _T("CD")) == 0) {
                Result = MakeProcessCd(ChildRecipe, ExecContext->CmdToExec.ArgC, ExecContext->CmdToExec.ArgV);
                ExecutedBuiltin = TRUE;
                ChildRecipe->ProcessHandle = NULL;
            } else {
                PYORI_LIBSH_BUILTIN_CALLBACK Callback;

                Callback = YoriLibShLookupBuiltinByName(&ExecContext->CmdToExec.ArgV[0]);
                if (Callback) {

                    SetCurrentDirectory(ChildRecipe->CurrentDirectory.StartOfString);
                    Result = MakeShExecuteInProc(Callback->BuiltInFn, ExecContext);
                    SetCurrentDirectory(MakeContext->ProcessCurrentDirectory.StartOfString);

                    ExecutedBuiltin = TRUE;
                    ChildRecipe->ProcessHandle = NULL;
                }
            }

            if (!ExecutedBuiltin && !Reparse) {
                for (Index = 0; Index < sizeof(MakePuntToCmd)/sizeof(MakePuntToCmd[0]); Index++) {
                    if (YoriLibCompareStringWithLiteralInsensitive(&ExecContext->CmdToExec.ArgV[0], MakePuntToCmd[Index]) == 0) {
                        PuntToCmd = TRUE;
                        break;
                    }
                }
            }

            if (Result != EXIT_SUCCESS && !CmdToExec->IgnoreErrors) {
                MakeFreeCmdContextIfNecessary(ChildRecipe);
                YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Failure to launch:\n%y\n"), &CmdToExec->Cmd);
                return FALSE;
            }
        }

        if (ExecutedBuiltin) {
            YoriLibFreeStringContents(&CmdToParse);
            return TRUE;
        }

        if (!Reparse) {
            break;
        }

        MakeFreeCmdContextIfNecessary(ChildRecipe);

        if (CmdToParse.LengthInChars == 0) {
            YoriLibFreeStringContents(&CmdToParse);
            ChildRecipe->ProcessHandle = NULL;
            return TRUE;
        }
    }

    //
    //  Check for unsupported operators.  Currently redirection can be
    //  handled within make, but it does not understand how to pipe commands
    //  or otherwise execute multi-command plans (|, ||, &&, &.)
    //

    if (!PuntToCmd) {
        ASSERT(ChildRecipe->CmdContextPresent);
        if (ChildRecipe->ExecPlan.NumberCommands > 1) {
            PuntToCmd = TRUE;
        }
    }

    //
    //  If it's not a known builtin, execute the command.  When punting to cmd
    //  this involves creating a string for it (which performs path lookup
    //  internally), but for anything else we perform path lookup below.
    //

    if (PuntToCmd) {

        MakeFreeCmdContextIfNecessary(ChildRecipe);

        if (!YoriLibShBuildCmdContextForCmdBuckPass(&ChildRecipe->CmdContext, &CmdToParse)) {
            YoriLibFreeStringContents(&CmdToParse);
            return FALSE;
        }

        if (!YoriLibShParseCmdContextToExecPlan(&ChildRecipe->CmdContext, &ChildRecipe->ExecPlan, NULL, NULL, NULL, NULL)) {
            YoriLibShFreeCmdContext(&ChildRecipe->CmdContext);
            return FALSE;
        }

        ChildRecipe->CmdContextPresent = TRUE;
    } else {
        YORI_STRING FoundInPath;

        ExecContext = ChildRecipe->ExecPlan.FirstCmd;

        YoriLibInitEmptyString(&FoundInPath);
        if (YoriLibLocateExecutableInPath(&ExecContext->CmdToExec.ArgV[0], NULL, NULL, &FoundInPath)) {
            if (FoundInPath.LengthInChars > 0) {
                YoriLibFreeStringContents(&ExecContext->CmdToExec.ArgV[0]);
                memcpy(&ExecContext->CmdToExec.ArgV[0], &FoundInPath, sizeof(YORI_STRING));
            } else {
                YoriLibFreeStringContents(&FoundInPath);
            }
        }
    }

    ExecContext = ChildRecipe->ExecPlan.FirstCmd;

    ASSERT(ChildRecipe->ExecPlan.NumberCommands == 1);
    if (ExecContext->StdOutType == StdOutTypeDefault) {
        ExecContext->StdOutType = StdOutTypeBuffer;
        if (ExecContext->StdErrType == StdErrTypeDefault) {
            ExecContext->StdErrType = StdErrTypeStdOut;
        }
    }

    ChildRecipe->JobId = MakeAllocateJobId(MakeContext);

    Error = YoriLibShCreateProcess(ExecContext,
                                   ChildRecipe->CurrentDirectory.StartOfString,
                                   &FailedInRedirection);

    if (Error != ERROR_SUCCESS) {
        MakeFreeJobId(MakeContext, ChildRecipe->JobId);
        ChildRecipe->ProcessHandle = NULL;
        if (!CmdToExec->IgnoreErrors) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Failure to launch:\n%y\n"), &CmdToParse);
            YoriLibFreeStringContents(&CmdToParse);
            MakeFreeCmdContextIfNecessary(ChildRecipe);
            return FALSE;
        }
    } else {
        CloseHandle(ExecContext->hPrimaryThread);
        ExecContext->hPrimaryThread = NULL;
        ChildRecipe->ProcessHandle = ExecContext->hProcess;
        YoriLibShCommenceProcessBuffersIfNeeded(ExecContext);
    }
    YoriLibFreeStringContents(&CmdToParse);

    return TRUE;
}

/**
 Launch the recipe for the next ready target.

 @param MakeContext Pointer to the context.

 @param ChildRecipe Pointer to the child process structure to populate with
        information about the child process that has been initiated.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOLEAN
MakeLaunchNextTarget(
    __in PMAKE_CONTEXT MakeContext,
    __inout PMAKE_CHILD_RECIPE ChildRecipe
    )
{
    PMAKE_TARGET Target;
    PYORI_LIST_ENTRY ListEntry;

    //
    //  This recipe structure should be available for use.
    //

    ASSERT(!ChildRecipe->CmdContextPresent);

    //
    //  Get the next target.
    //

    ListEntry = YoriLibGetNextListEntry(&MakeContext->TargetsReady, NULL);

    //
    //  The caller should have checked that there is a target.
    //

    ASSERT(ListEntry != NULL);
    if (ListEntry == NULL) {
        return FALSE;
    }

    YoriLibRemoveListItem(ListEntry);
    YoriLibAppendList(&MakeContext->TargetsRunning, ListEntry);
    Target = CONTAINING_RECORD(ListEntry, MAKE_TARGET, RebuildList);

    ChildRecipe->Target = Target;
    ChildRecipe->Cmd = NULL;

    //
    //  The previous recipe should have been cleaned up.
    //

    ASSERT(ChildRecipe->CurrentDirectory.LengthInChars == 0);
    ASSERT(ChildRecipe->CurrentDirectory.MemoryToFree == NULL);

    //
    //  Initialize the recipe current directory as the scope context
    //  directory.  The recipe is able to change it.
    //

    YoriLibCloneString(&ChildRecipe->CurrentDirectory, &Target->ScopeContext->HashEntry.Key);
    if (YoriLibIsPathPrefixed(&ChildRecipe->CurrentDirectory)) {
        ChildRecipe->CurrentDirectory.StartOfString += sizeof("\\\\.\\") - 1;
        ChildRecipe->CurrentDirectory.LengthInChars -= sizeof("\\\\.\\") - 1;
        ChildRecipe->CurrentDirectory.LengthAllocated -= sizeof("\\\\.\\") - 1;
    }

    ASSERT(YoriLibIsStringNullTerminated(&ChildRecipe->CurrentDirectory));

    return MakeLaunchNextCmd(MakeContext, ChildRecipe);
}

/**
 Indicate that an entire recipe has completed.  This may have succeeded, or
 may have failed partway.  This function needs to clean up regardless.

 @param MakeContext Pointer to the make context.

 @param ChildRecipe Pointer to the recipe to clean up for reuse.
 */
VOID
MakeRecipeCompletion(
    __in PMAKE_CONTEXT MakeContext,
    __inout PMAKE_CHILD_RECIPE ChildRecipe
    )
{
    UNREFERENCED_PARAMETER(MakeContext);

    //
    //  This recipe structure should not have child processes executing.
    //

    ASSERT(!ChildRecipe->CmdContextPresent);
    YoriLibFreeStringContents(&ChildRecipe->CurrentDirectory);
}

/**
 Update the dependency graph to ensure that any targets waiting for the
 specified target can now be executed.

 @param MakeContext Pointer to the context.

 @param Target Pointer to the target that has completed.
 */
VOID
MakeUpdateDependenciesForTarget(
    __in PMAKE_CONTEXT MakeContext,
    __in PMAKE_TARGET Target
    )
{
    PYORI_LIST_ENTRY ListEntry;
    PMAKE_TARGET_DEPENDENCY Dependency;

    YoriLibRemoveListItem(&Target->RebuildList);
    YoriLibAppendList(&MakeContext->TargetsFinished, &Target->RebuildList);

    ListEntry = NULL;
    ListEntry = YoriLibGetNextListEntry(&Target->ChildDependents, ListEntry);
    while (ListEntry != NULL) {
        Dependency = CONTAINING_RECORD(ListEntry, MAKE_TARGET_DEPENDENCY, ParentDependents);
        if (Dependency->Child->RebuildRequired) {
            Dependency->Child->NumberParentsToBuild--;
            if (Dependency->Child->NumberParentsToBuild == 0) {
                YoriLibRemoveListItem(&Dependency->Child->RebuildList);
                YoriLibAppendList(&MakeContext->TargetsReady, &Dependency->Child->RebuildList);
            }
        }
        ListEntry = YoriLibGetNextListEntry(&Target->ChildDependents, ListEntry);
    }
}

/**
 After a child process has completed, indicate whether it succeeded or
 failed.  A failed process that the makefile indicates is allowed to fail
 is considered success.

 @param MakeContext Pointer to the make context.

 @param ChildRecipe Pointer to the child that has completed.

 @return TRUE to indicate the process succeeded, FALSE to indicate the process
         failed.
 */
BOOLEAN
MakeProcessCompletion(
    __in PMAKE_CONTEXT MakeContext,
    __in PMAKE_CHILD_RECIPE ChildRecipe
    )
{
    DWORD ExitCode;
    PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext;
    YORI_STRING ProcessOutput;
    BOOLEAN Result;
    BOOLEAN RestoreColor;
    WORD DefaultColor;

    ExitCode = EXIT_SUCCESS;

    Result = TRUE;

    //
    //  Ideally this would wait for the process buffer threads rather than
    //  wait for process termination, then get here and wait for the process
    //  buffer threads.  Unfortunately since there are two threads and we're
    //  hitting the 64 object wait limit, it seems like the lesser evil.
    //

    if (ChildRecipe->ProcessHandle != NULL) {

        ExitCode = 255;
        GetExitCodeProcess(ChildRecipe->ProcessHandle, &ExitCode);
        ASSERT(ChildRecipe->CmdContextPresent);

        DefaultColor = YoriLibVtGetDefaultColor();
        RestoreColor = FALSE;

        if (ExitCode != 0) {

            if (!ChildRecipe->Cmd->IgnoreErrors) {
                YoriLibVtSetConsoleTextAttribute(YORI_LIB_OUTPUT_STDOUT, (WORD)((DefaultColor & 0xF0) | FOREGROUND_RED | FOREGROUND_INTENSITY));
                YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Error in target: %y\n"), &ChildRecipe->Target->HashEntry.Key);
                Result = FALSE;
            } else {
                YoriLibVtSetConsoleTextAttribute(YORI_LIB_OUTPUT_STDOUT, (WORD)((DefaultColor & 0xF0) | FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY));
            }

            RestoreColor = TRUE;
        }

        //
        //  If the output was being sent to a buffer owned by make, display
        //  the result now, in a different color if the process failed.
        //  Note at this point execution is serialized - this might prevent
        //  child processes from being launched, but output will be ordered.
        //

        ExecContext = ChildRecipe->ExecPlan.FirstCmd;

        if (ExecContext->StdOutType == StdOutTypeBuffer) {
            YoriLibShWaitForProcessBufferToFinalize(ExecContext->StdOut.Buffer.ProcessBuffers);
            if (YoriLibShGetProcessOutputBuffer(ExecContext->StdOut.Buffer.ProcessBuffers, &ProcessOutput)) {
                if (ProcessOutput.LengthInChars > 0) {
                    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%y"), &ProcessOutput);
                }
                YoriLibFreeStringContents(&ProcessOutput);
            }

            YoriLibShTeardownProcessBuffersIfCompleted(ExecContext->StdOut.Buffer.ProcessBuffers);
        } else {
            ASSERT(ExecContext->StdErrType != StdErrTypeBuffer);
        }

        if (!ChildRecipe->Cmd->IgnoreErrors && ExitCode != 0) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Command:\n%y\n"), &ChildRecipe->Cmd->Cmd);
        }

        if (RestoreColor) {
            YoriLibVtSetConsoleTextAttribute(YORI_LIB_OUTPUT_STDOUT, YoriLibVtGetDefaultColor());
        }

        //
        //  This is closed implicitly as part of FreeExecPlan below
        //

        ASSERT(ChildRecipe->CmdContextPresent);
        ChildRecipe->ProcessHandle = NULL;
        MakeFreeJobId(MakeContext, ChildRecipe->JobId);
    }

    MakeFreeCmdContextIfNecessary(ChildRecipe);

    return Result;
}


/**
 Remove all targets that are in the front of the ready queue but really have
 no actions to perform.

 MSFIX This process should probably occur earlier, when a target moves from
 waiting it can move directly to completed if there is nothing to do.  This
 allows targets that depend on it to immediately become ready rather than
 waiting for that target to reach the front of the queue before realizing
 that there are other actions that can be performed.

 @param MakeContext Pointer to the context.

 @return TRUE to indicate one or more targets were processed, FALSE to
         indicate that the next target has an action to perform.
 */
BOOLEAN
MakeCompleteReadyWithNoRecipe(
    __in PMAKE_CONTEXT MakeContext
    )
{
    PYORI_LIST_ENTRY ListEntry;
    PMAKE_TARGET Target;
    BOOLEAN RemovedItem;

    RemovedItem = FALSE;
    ListEntry = YoriLibGetNextListEntry(&MakeContext->TargetsReady, NULL);
    while (ListEntry != NULL) {
        Target = CONTAINING_RECORD(ListEntry, MAKE_TARGET, RebuildList);
        if (YoriLibIsListEmpty(&Target->ExecCmds)) {
            RemovedItem = TRUE;
            MakeUpdateDependenciesForTarget(MakeContext, Target);
        } else {
            break;
        }
        ListEntry = YoriLibGetNextListEntry(&MakeContext->TargetsReady, NULL);
    }

    return RemovedItem;
}

/**
 Execute commands required to build the requested target.

 @param MakeContext Pointer to the context.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOLEAN
MakeExecuteRequiredTargets(
    __in PMAKE_CONTEXT MakeContext
    )
{

    DWORD NumberActiveProcesses;
    DWORD Index;
    HANDLE *ProcessHandleArray;
    PMAKE_CHILD_RECIPE ChildRecipeArray;
    BOOLEAN Result;
    BOOLEAN MoveToNextTarget;
    BOOLEAN TargetFailureObserved;

    NumberActiveProcesses = 0;
    TargetFailureObserved = FALSE;

    ProcessHandleArray = YoriLibMalloc(MakeContext->NumberProcesses * sizeof(HANDLE));
    if (ProcessHandleArray == NULL) {
        return FALSE;
    }

    ZeroMemory(ProcessHandleArray, MakeContext->NumberProcesses * sizeof(HANDLE));

    ChildRecipeArray = YoriLibMalloc(MakeContext->NumberProcesses * sizeof(MAKE_CHILD_RECIPE));
    if (ChildRecipeArray == NULL) {
        YoriLibFree(ProcessHandleArray);
        return FALSE;
    }

    ZeroMemory(ChildRecipeArray, MakeContext->NumberProcesses * sizeof(MAKE_CHILD_RECIPE));
    Result = TRUE;

    while (TRUE) {

        while (NumberActiveProcesses < MakeContext->NumberProcesses && !YoriLibIsListEmpty(&MakeContext->TargetsReady)) {
            if (!MakeCompleteReadyWithNoRecipe(MakeContext)) {
                if (!MakeLaunchNextTarget(MakeContext, &ChildRecipeArray[NumberActiveProcesses])) {
                    Result = FALSE;
                    goto Drain;
                }
                NumberActiveProcesses++;
            }
        }

        while (NumberActiveProcesses == MakeContext->NumberProcesses || YoriLibIsListEmpty(&MakeContext->TargetsReady)) {

            if (NumberActiveProcesses == 0) {
                break;
            }

            //
            //  A process handle can be NULL if either a command failed to
            //  launch but was prefixed with - indicating failures should be
            //  ignored; or if it's a builtin command that completed
            //  synchronously.  In either case rather than wait, just process
            //  as if this command completed and move to the next command or
            //  target.
            //

            for (Index = 0; Index < NumberActiveProcesses; Index++) {
                ProcessHandleArray[Index] = ChildRecipeArray[Index].ProcessHandle;
                if (ProcessHandleArray[Index] == NULL) {
                    break;
                }
            }

            if (Index == NumberActiveProcesses) {
                Index = WaitForMultipleObjects(NumberActiveProcesses, ProcessHandleArray, FALSE, INFINITE);
                Index = Index - WAIT_OBJECT_0;
            }

            //
            //  Check if the process succeeded.  If so, and there are more
            //  commands for the target, try to launch those.
            //

            MoveToNextTarget = TRUE;
            Result = MakeProcessCompletion(MakeContext, &ChildRecipeArray[Index]);
            if (Result) {
                if (MakeDoesTargetHaveMoreCommands(&ChildRecipeArray[Index])) {
                    if (MakeLaunchNextCmd(MakeContext, &ChildRecipeArray[Index])) {
                        MoveToNextTarget = FALSE;
                    } else {
                        Result = FALSE;
                    }
                } else {
                    MakeRecipeCompletion(MakeContext, &ChildRecipeArray[Index]);
                }
            }

            //
            //  If we are moving to the next target, overwrite this child
            //  process with the executing ones, and launch a new target.
            //  If we failed, we still need to move up all of the executing
            //  ones to facilitate drain.
            //

            if (MoveToNextTarget) {
                if (Result) {
                    MakeUpdateDependenciesForTarget(MakeContext, ChildRecipeArray[Index].Target);
                } else {
                    MakeRecipeCompletion(MakeContext, &ChildRecipeArray[Index]);
                }

                if (NumberActiveProcesses > Index + 1) {
                    memmove(&ChildRecipeArray[Index],
                            &ChildRecipeArray[Index + 1],
                            (NumberActiveProcesses - Index - 1) * sizeof(MAKE_CHILD_RECIPE));
                }

                NumberActiveProcesses--;

                //
                //  Everything in the final entry has been moved to earlier
                //  entries, so the final entry is uninitialized.
                //

                ZeroMemory(&ChildRecipeArray[NumberActiveProcesses], sizeof(MAKE_CHILD_RECIPE));
            }

            if (Result == FALSE) {
                if (MakeContext->KeepGoing) {
                    TargetFailureObserved = TRUE;
                } else {
                    goto Drain;
                }
            }
        }

        //
        //  Nothing failed to launch, nothing returned a failure code, all
        //  ready targets have been launched and completed.  There shouldn't
        //  be anything left to do or something is horribly wrong.
        //

        if (NumberActiveProcesses == 0 && YoriLibIsListEmpty(&MakeContext->TargetsReady)) {
            ASSERT(MakeContext->KeepGoing || YoriLibIsListEmpty(&MakeContext->TargetsWaiting));
            break;
        }
    }

    if (TargetFailureObserved) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Failures encountered, build incomplete\n"));
    }

Drain:

    while (NumberActiveProcesses > 0) {
        for (Index = 0; Index < NumberActiveProcesses; Index++) {
            if (ChildRecipeArray[Index].ProcessHandle == NULL) {
                break;
            }
            ProcessHandleArray[Index] = ChildRecipeArray[Index].ProcessHandle;
        }

        if (Index == NumberActiveProcesses) {
            Index = WaitForMultipleObjects(NumberActiveProcesses, ProcessHandleArray, FALSE, INFINITE);
            Index = Index - WAIT_OBJECT_0;

            MakeProcessCompletion(MakeContext, &ChildRecipeArray[Index]);
            MakeRecipeCompletion(MakeContext, &ChildRecipeArray[Index]);
        }

        if (NumberActiveProcesses > Index + 1) {
            memmove(&ChildRecipeArray[Index],
                    &ChildRecipeArray[Index + 1],
                    (NumberActiveProcesses - Index - 1) * sizeof(MAKE_CHILD_RECIPE));
        }

        NumberActiveProcesses--;
    }

    YoriLibFree(ChildRecipeArray);
    YoriLibFree(ProcessHandleArray);

    return Result;
}

// vim:sw=4:ts=4:et:


<|start_filename|>petool/petool.c<|end_filename|>
/**
 * @file petool/petool.c
 *
 * Yori shell PE tool for manipulating PE files
 *
 * Copyright (c) 2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 Help text to display to the user.
 */
const
CHAR strPeToolHelpText[] =
        "\n"
        "Manage PE files.\n"
        "\n"
        "PETOOL [-license]\n"
        "PETOOL -c file\n"
        "PETOOL -cu file\n"
        "\n"
        "   -c             Calculate the PE checksum for a binary\n"
        "   -cu            Update the checksum in the PE header from contents\n";

/**
 Display usage text to the user.
 */
BOOL
PeToolHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("PeTool %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strPeToolHelpText);
    return TRUE;
}

/**
 Calculate and display the checksum for a specified file.

 @param FileName Pointer to the file name to calculate a checksum for.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOLEAN
PeToolCalculateChecksum(
    __in PYORI_STRING FileName
    )
{
    DWORD Err;
    DWORD HeaderChecksum;
    DWORD DataChecksum;
    YORI_STRING FullPath;

    YoriLibLoadImageHlpFunctions();

    if (DllImageHlp.pMapFileAndCheckSumW == NULL) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("petool: OS support not present\n"));
        return FALSE;
    }

    YoriLibInitEmptyString(&FullPath);
    if (!YoriLibUserStringToSingleFilePath(FileName, TRUE, &FullPath)) {
        return FALSE;
    }

    Err = DllImageHlp.pMapFileAndCheckSumW(FullPath.StartOfString, &HeaderChecksum, &DataChecksum);
    if (Err != 0) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Open of source failed: %y\n"), FileName);
        YoriLibFreeStringContents(&FullPath);
        return FALSE;
    }

    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Checksum in PE header: %08x\n")
                                          _T("Checksum of file contents: %08x\n"),
                                          HeaderChecksum,
                                          DataChecksum);

    YoriLibFreeStringContents(&FullPath);
    return TRUE;
}

/**
 Generate the checksum for a binary and update the header.

 @param FileName Pointer to the file name to generate a checksum for.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOLEAN
PeToolUpdateChecksum(
    __in PYORI_STRING FileName
    )
{
    DWORD Err;
    DWORD HeaderChecksum;
    DWORD DataChecksum;
    DWORD PeHeaderOffset;
    HANDLE hFileRead;
    union {
        IMAGE_DOS_HEADER DosHeader;
        YORILIB_PE_HEADERS PeHeaders;
    } u;
    DWORD BytesReturned;
    YORI_STRING FullPath;

    YoriLibLoadImageHlpFunctions();

    if (DllImageHlp.pMapFileAndCheckSumW == NULL) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("petool: OS support not present\n"));
        return FALSE;
    }

    YoriLibInitEmptyString(&FullPath);
    if (!YoriLibUserStringToSingleFilePath(FileName, TRUE, &FullPath)) {
        return FALSE;
    }

    Err = DllImageHlp.pMapFileAndCheckSumW(FullPath.StartOfString, &HeaderChecksum, &DataChecksum);
    if (Err != 0) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Open of source failed: %y\n"), FileName);
        YoriLibFreeStringContents(&FullPath);
        return FALSE;
    }


    ASSERT(YoriLibIsStringNullTerminated(&FullPath));

    //
    //  We want the earlier handle to be attribute only so we can
    //  operate on directories, but we need data for this, so we
    //  end up with two handles.
    //

    hFileRead = CreateFile(FullPath.StartOfString,
                           FILE_READ_ATTRIBUTES|FILE_READ_DATA|FILE_WRITE_DATA,
                           FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
                           NULL,
                           OPEN_EXISTING,
                           FILE_FLAG_BACKUP_SEMANTICS,
                           NULL);

    if (hFileRead == INVALID_HANDLE_VALUE) {
        LPTSTR ErrText;
        Err = GetLastError();
        ErrText = YoriLibGetWinErrorText(Err);
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Open of source failed: %y: %s"), &FullPath, ErrText);
        YoriLibFreeStringContents(&FullPath);
        YoriLibFreeWinErrorText(ErrText);
        return FALSE;
    }


    if (ReadFile(hFileRead, &u.DosHeader, sizeof(u.DosHeader), &BytesReturned, NULL) &&
        BytesReturned == sizeof(u.DosHeader) &&
        u.DosHeader.e_magic == IMAGE_DOS_SIGNATURE &&
        u.DosHeader.e_lfanew != 0) {

        PeHeaderOffset = u.DosHeader.e_lfanew;
        SetFilePointer(hFileRead, PeHeaderOffset, NULL, FILE_BEGIN);

        if (ReadFile(hFileRead, &u.PeHeaders, sizeof(YORILIB_PE_HEADERS), &BytesReturned, NULL) &&
            BytesReturned == sizeof(YORILIB_PE_HEADERS) &&
            u.PeHeaders.Signature == IMAGE_NT_SIGNATURE &&
            u.PeHeaders.ImageHeader.SizeOfOptionalHeader >= FIELD_OFFSET(IMAGE_OPTIONAL_HEADER, CheckSum) + sizeof(u.PeHeaders.OptionalHeader.CheckSum)) {

            u.PeHeaders.OptionalHeader.CheckSum = DataChecksum;
            SetFilePointer(hFileRead, PeHeaderOffset, NULL, FILE_BEGIN);

            if (!WriteFile(hFileRead, &u.PeHeaders, sizeof(YORILIB_PE_HEADERS), &BytesReturned, NULL)) {
                CloseHandle(hFileRead);
                YoriLibFreeStringContents(&FullPath);
                return FALSE;
            }
        }
    }

    CloseHandle(hFileRead);
    YoriLibFreeStringContents(&FullPath);

    return TRUE;
}

/**
 A set of operations supported by this application.
 */
typedef enum _PETOOL_OP {
    PeToolOpNone = 0,
    PeToolOpCalculateChecksum = 1,
    PeToolOpUpdateChecksum = 2
} PETOOL_OP;

#ifdef YORI_BUILTIN
/**
 The main entrypoint for the petool builtin command.
 */
#define ENTRYPOINT YoriCmd_PETOOL
#else
/**
 The main entrypoint for the petool standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the petool cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the child process on success, or failure if the child
         could not be launched.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg;
    YORI_STRING Arg;
    PYORI_STRING FileName = NULL;
    PETOOL_OP Op;
    DWORD Result;

    Op = PeToolOpNone;

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                PeToolHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2021"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("c")) == 0) {
                if (ArgC > i + 1) {
                    FileName = &ArgV[i + 1];
                    Op = PeToolOpCalculateChecksum;
                    ArgumentUnderstood = TRUE;
                }
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("cu")) == 0) {
                if (ArgC > i + 1) {
                    FileName = &ArgV[i + 1];
                    Op = PeToolOpUpdateChecksum;
                    ArgumentUnderstood = TRUE;
                }
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    if (Op == PeToolOpNone) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("petool: operation not specified\n"));
        return EXIT_FAILURE;
    }

    Result = EXIT_SUCCESS;

    switch(Op) {
        case PeToolOpCalculateChecksum:
            if (!PeToolCalculateChecksum(FileName)) {
                Result = EXIT_FAILURE;
            }
            break;
        case PeToolOpUpdateChecksum:
            if (!PeToolUpdateChecksum(FileName)) {
                Result = EXIT_FAILURE;
            }
            break;
    }

    return Result;
}

// vim:sw=4:ts=4:et:


<|start_filename|>make/minish.c<|end_filename|>
/**
 * @file make/minish.c
 *
 * Yori make stripped down shell logic
 *
 * Copyright (c) 2017-2019 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>
#include <yorish.h>
#include "make.h"

//
//  The functions here are cut down variants of the ones in sh/exec.c.
//  The regular shell needs to support scenarios that make no sense for
//  make - things like executing programs through ShellExecute to
//  prompt for elevation, job control, or even in-proc modules don't make
//  much sense here.  There is a fuzzy line between what's in and out
//  though, so potentially more code could migrate here or to libsh to
//  enable it to be shared.
//


/**
 Execute a single program.  If the execution is synchronous, this routine will
 wait for the program to complete and return its exit code.  If the execution
 is not synchronous, this routine will return without waiting and provide zero
 as a (not meaningful) exit code.

 @param ExecContext The context of a single program to execute.

 @return The exit code of the program, when executed synchronously.
 */
DWORD
MakeShExecuteSingleProgram(
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext
    )
{
    DWORD ExitCode = 0;
    BOOL FailedInRedirection = FALSE;
    DWORD Err;

    Err = YoriLibShCreateProcess(ExecContext, NULL, &FailedInRedirection);

    if (Err != NO_ERROR) {
        LPTSTR ErrText = YoriLibGetWinErrorText(Err);
        if (FailedInRedirection) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Failed to initialize redirection: %s"), ErrText);
        } else {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("CreateProcess failed: %s"), ErrText);
        }
        YoriLibFreeWinErrorText(ErrText);
        YoriLibShCleanupFailedProcessLaunch(ExecContext);
        return 1;
    }

    YoriLibShCommenceProcessBuffersIfNeeded(ExecContext);

    ASSERT(ExecContext->hProcess != NULL);
    if (ExecContext->WaitForCompletion) {
        WaitForSingleObject(ExecContext->hProcess, INFINITE);
        GetExitCodeProcess(ExecContext->hProcess, &ExitCode);
    }
    return ExitCode;
}

/**
 Call a builtin function.  This is part of the main excecutable and is
 executed synchronously via a call rather than a CreateProcess.

 @param Fn The function associated with the builtin operation to call.

 @param ExecContext The context surrounding this particular function.

 @return ExitCode, typically zero for success, nonzero for failure.
 */
DWORD
MakeShExecuteInProc(
    __in PYORI_CMD_BUILTIN Fn,
    __in PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext
    )
{
    YORI_LIBSH_PREVIOUS_REDIRECT_CONTEXT PreviousRedirectContext;
    BOOLEAN WasPipe = FALSE;
    PYORI_LIBSH_CMD_CONTEXT OriginalCmdContext = &ExecContext->CmdToExec;
    YORI_STRING CmdLine;
    PYORI_STRING EscapedArgV;
    PYORI_STRING NoEscapedArgV;
    DWORD ArgC;
    DWORD Count;
    DWORD ExitCode = 0;

    EscapedArgV = NULL;
    NoEscapedArgV = NULL;
    ArgC = 0;

    //
    //  Build a command line, leaving all escapes in the command line.
    //

    YoriLibInitEmptyString(&CmdLine);
    if (!YoriLibShBuildCmdlineFromCmdContext(OriginalCmdContext, &CmdLine, FALSE, NULL, NULL)) {
        ExitCode = ERROR_OUTOFMEMORY;
        goto Cleanup;
    }

    //
    //  Parse the command line in the same way that a child process would.
    //

    ASSERT(YoriLibIsStringNullTerminated(&CmdLine));
    EscapedArgV = YoriLibCmdlineToArgcArgv(CmdLine.StartOfString, (DWORD)-1, TRUE, &ArgC);
    YoriLibFreeStringContents(&CmdLine);

    if (EscapedArgV == NULL) {
        ExitCode = ERROR_OUTOFMEMORY;
        goto Cleanup;
    }

    //
    //  Remove the escapes from the command line.  This allows the builtin to
    //  have access to the escaped form if required.
    //

    NoEscapedArgV = YoriLibReferencedMalloc(ArgC * sizeof(YORI_STRING));
    if (NoEscapedArgV == NULL) {
        ExitCode = ERROR_OUTOFMEMORY;
        goto Cleanup;
    }

    for (Count = 0; Count < ArgC; Count++) {
        YoriLibCloneString(&NoEscapedArgV[Count], &EscapedArgV[Count]);
    }

    if (!YoriLibShRemoveEscapesFromArgCArgV(ArgC, NoEscapedArgV)) {
        ExitCode = ERROR_OUTOFMEMORY;
        goto Cleanup;
    }

    //
    //  We execute builtins on a single thread due to the amount of
    //  process wide state that could get messed up if we don't (eg.
    //  stdout.)  Unfortunately this means we can't natively implement
    //  things like pipe from builtins, because the builtin has to
    //  finish before the next process can start.  So if a pipe is
    //  requested, convert it into a buffer, and let the process
    //  finish.
    //

    if (ExecContext->StdOutType == StdOutTypePipe) {
        WasPipe = TRUE;
        ExecContext->StdOutType = StdOutTypeBuffer;
    }

    ExitCode = YoriLibShInitializeRedirection(ExecContext, TRUE, &PreviousRedirectContext);
    if (ExitCode != ERROR_SUCCESS) {
        goto Cleanup;
    }

    //
    //  Unlike external processes, builtins need to start buffering
    //  before they start to ensure that output during execution has
    //  somewhere to go.
    //

    if (ExecContext->StdOutType == StdOutTypeBuffer) {
        if (ExecContext->StdOut.Buffer.ProcessBuffers != NULL) {
            if (YoriLibShAppendToExistingProcessBuffer(ExecContext)) {
                ExecContext->StdOut.Buffer.PipeFromProcess = NULL;
            } else {
                ExecContext->StdOut.Buffer.ProcessBuffers = NULL;
            }
        } else {
            if (YoriLibShCreateNewProcessBuffer(ExecContext)) {
                ExecContext->StdOut.Buffer.PipeFromProcess = NULL;
            }
        }
    }

    ExitCode = Fn(ArgC, NoEscapedArgV);
    YoriLibShRevertRedirection(&PreviousRedirectContext);

    if (WasPipe) {
        YoriLibShForwardProcessBufferToNextProcess(ExecContext);
    } else {

        //
        //  Once the process has completed, if it's outputting to
        //  buffers, wait for the buffers to contain final data.
        //

        if (ExecContext->StdOutType == StdOutTypeBuffer &&
            ExecContext->StdOut.Buffer.ProcessBuffers != NULL)  {

            YoriLibShWaitForProcessBufferToFinalize(ExecContext->StdOut.Buffer.ProcessBuffers);
        }

        if (ExecContext->StdErrType == StdErrTypeBuffer &&
            ExecContext->StdErr.Buffer.ProcessBuffers != NULL) {

            YoriLibShWaitForProcessBufferToFinalize(ExecContext->StdErr.Buffer.ProcessBuffers);
        }
    }

Cleanup:

    if (NoEscapedArgV != NULL) {
        for (Count = 0; Count < ArgC; Count++) {
            YoriLibFreeStringContents(&NoEscapedArgV[Count]);
        }
        YoriLibDereference(NoEscapedArgV);
    }

    if (EscapedArgV != NULL) {
        for (Count = 0; Count < ArgC; Count++) {
            YoriLibFreeStringContents(&EscapedArgV[Count]);
        }
        YoriLibDereference(EscapedArgV);
    }

    return ExitCode;
}

/**
 Cancel an exec plan.  This is invoked after the user hits Ctrl+C and attempts
 to terminate all outstanding processes associated with the request.

 @param ExecPlan The plan to terminate all outstanding processes in.
 */
VOID
MakeShCancelExecPlan(
    __in PYORI_LIBSH_EXEC_PLAN ExecPlan
    )
{
    PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext;

    //
    //  Loop and ask the processes nicely to terminate.
    //

    ExecContext = ExecPlan->FirstCmd;
    while (ExecContext != NULL) {
        if (ExecContext->hProcess != NULL) {
            if (WaitForSingleObject(ExecContext->hProcess, 0) != WAIT_OBJECT_0) {
                if (ExecContext->dwProcessId != 0) {
                    GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, ExecContext->dwProcessId);
                }
            }
        }
        ExecContext = ExecContext->NextProgram;
    }

    Sleep(50);

    //
    //  Loop again and ask the processes less nicely to terminate.
    //

    ExecContext = ExecPlan->FirstCmd;
    while (ExecContext != NULL) {
        if (ExecContext->hProcess != NULL) {
            if (WaitForSingleObject(ExecContext->hProcess, 0) != WAIT_OBJECT_0) {
                TerminateProcess(ExecContext->hProcess, 1);
            }
        }
        ExecContext = ExecContext->NextProgram;
    }

    //
    //  Loop waiting for any debugger threads to terminate.  These are
    //  referencing the ExecContext so it's important that they're
    //  terminated before we start freeing them.
    //

    ExecContext = ExecPlan->FirstCmd;
    while (ExecContext != NULL) {
        if (ExecContext->hDebuggerThread != NULL) {
            WaitForSingleObject(ExecContext->hDebuggerThread, INFINITE);
        }
        ExecContext = ExecContext->NextProgram;
    }
}

/**
 Execute an exec plan.  An exec plan has multiple processes, including
 different pipe and redirection operators.  Optionally return the result
 of any output buffered processes in the plan, to facilitate back quotes.

 @param ExecPlan Pointer to the exec plan to execute.

 @param OutputBuffer On successful completion, updated to point to the
        resulting output buffer.
 */
DWORD
MakeShExecExecPlan(
    __in PYORI_LIBSH_EXEC_PLAN ExecPlan,
    __out_opt PVOID * OutputBuffer
    )
{
    PYORI_LIBSH_SINGLE_EXEC_CONTEXT ExecContext;
    PVOID PreviouslyObservedOutputBuffer = NULL;
    PYORI_LIBSH_BUILTIN_CALLBACK Callback;
    YORI_STRING FoundInPath;
    DWORD ExitCode = 0;

    ExecContext = ExecPlan->FirstCmd;
    while (ExecContext != NULL) {

        //
        //  If some previous program in the plan has output to a buffer, use
        //  the same buffer for any later program which intends to output to
        //  a buffer.
        //

        if (ExecContext->StdOutType == StdOutTypeBuffer &&
            ExecContext->WaitForCompletion) {

            ExecContext->StdOut.Buffer.ProcessBuffers = PreviouslyObservedOutputBuffer;
        }

        if (YoriLibIsOperationCancelled()) {
            break;
        }

        ExitCode = 255;
        Callback = YoriLibShLookupBuiltinByName(&ExecContext->CmdToExec.ArgV[0]);
        if (Callback != NULL) {
            ExitCode = MakeShExecuteInProc(Callback->BuiltInFn, ExecContext);
        } else {
            YoriLibInitEmptyString(&FoundInPath);
            if (YoriLibLocateExecutableInPath(&ExecContext->CmdToExec.ArgV[0], NULL, NULL, &FoundInPath)) {
                if (FoundInPath.LengthInChars > 0) {
                    YoriLibFreeStringContents(&ExecContext->CmdToExec.ArgV[0]);
                    memcpy(&ExecContext->CmdToExec.ArgV[0], &FoundInPath, sizeof(YORI_STRING));

                    ExitCode = MakeShExecuteSingleProgram(ExecContext);
                } else {
                    YoriLibFreeStringContents(&FoundInPath);
                }
            }
        }

        //
        //  If the program output back to a shell owned buffer and we waited
        //  for it to complete, we can use the same buffer for later commands
        //  in the set.
        //

        if (ExecContext->StdOutType == StdOutTypeBuffer &&
            ExecContext->StdOut.Buffer.ProcessBuffers != NULL &&
            ExecContext->WaitForCompletion) {

            PreviouslyObservedOutputBuffer = ExecContext->StdOut.Buffer.ProcessBuffers;
        }

        //
        //  Determine which program to execute next, if any.
        //

        if (ExecContext->NextProgram != NULL) {
            switch(ExecContext->NextProgramType) {
                case NextProgramExecUnconditionally:
                    ExecContext = ExecContext->NextProgram;
                    break;
                case NextProgramExecConcurrently:
                    ExecContext = ExecContext->NextProgram;
                    break;
                case NextProgramExecOnFailure:
                    if (ExitCode != 0) {
                        ExecContext = ExecContext->NextProgram;
                    } else {
                        do {
                            ExecContext = ExecContext->NextProgram;
                        } while (ExecContext != NULL && (ExecContext->NextProgramType == NextProgramExecOnFailure || ExecContext->NextProgramType == NextProgramExecConcurrently));
                        if (ExecContext != NULL) {
                            ExecContext = ExecContext->NextProgram;
                        }
                    }
                    break;
                case NextProgramExecOnSuccess:
                    if (ExitCode == 0) {
                        ExecContext = ExecContext->NextProgram;
                    } else {
                        do {
                            ExecContext = ExecContext->NextProgram;
                        } while (ExecContext != NULL && (ExecContext->NextProgramType == NextProgramExecOnSuccess || ExecContext->NextProgramType == NextProgramExecConcurrently));
                        if (ExecContext != NULL) {
                            ExecContext = ExecContext->NextProgram;
                        }
                    }
                    break;
                case NextProgramExecNever:
                    ExecContext = NULL;
                    break;
                default:
                    ASSERT(!"YoriLibShParseCmdContextToExecPlan created a plan that MakeShExecExecPlan doesn't know how to execute");
                    ExecContext = NULL;
                    break;
            }
        } else {
            ExecContext = NULL;
        }
    }

    if (OutputBuffer != NULL) {
        *OutputBuffer = PreviouslyObservedOutputBuffer;
    }

    if (YoriLibIsOperationCancelled()) {
        MakeShCancelExecPlan(ExecPlan);
    }

    return ExitCode;
}

// vim:sw=4:ts=4:et:


<|start_filename|>lib/cmdline.c<|end_filename|>
/**
 * @file lib/cmdline.c
 *
 * Converts between strings and argc/argv arrays
 *
 * Copyright (c) 2017-2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "yoripch.h"
#include "yorilib.h"

/**
 Returns TRUE if the character should be treated as indicating a command
 line option.

 @param Char The character to check.

 @return TRUE if the character indicates a command line option, FALSE if
         if it does not.
 */
BOOL
YoriLibIsCommandLineOptionChar(
    __in TCHAR Char
    )
{
    if (Char == '/' || Char == '-') {
        return TRUE;
    }
    return FALSE;
}

/**
 Returns TRUE if the string commences with a character indicating a command
 line option, and returns the remainder of the string.

 @param String The string to check.

 @param Arg On successful completion, if the string is an option, this Arg
        contains the option string.

 @return TRUE if the character indicates a command line option, FALSE if
         if it does not.
 */
__success(return)
BOOL
YoriLibIsCommandLineOption(
    __in PYORI_STRING String,
    __out PYORI_STRING Arg
    )
{
    if (String->LengthInChars < 1) {
        return FALSE;
    }
    if (YoriLibIsCommandLineOptionChar(String->StartOfString[0])) {
        YoriLibInitEmptyString(Arg);
        Arg->StartOfString = &String->StartOfString[1];
        Arg->LengthInChars = String->LengthInChars - 1;
        return TRUE;
    }
    return FALSE;
}


/**
 Check if an argument contains spaces and now requires quoting.

 @param Arg The argument to check.

 @return TRUE if quoting is required, FALSE if not.
 */
BOOLEAN
YoriLibCheckIfArgNeedsQuotes(
    __in PYORI_STRING Arg
    )
{
    BOOLEAN HasWhiteSpace;
    DWORD i;

    if (Arg->LengthInChars > 0 &&
        Arg->StartOfString[0] == '"') {

        return FALSE;
    }

    HasWhiteSpace = FALSE;
    for (i = 0; i < Arg->LengthInChars; i++) {
        if (Arg->StartOfString[i] == ' ') {
            HasWhiteSpace = TRUE;
            break;
        }
    }

    if (HasWhiteSpace) {
        return TRUE;
    }

    return FALSE;
}

/**
 This routine creates a command line string from a series of argc/argv style
 arguments described with yori strings.  The caller is expected to free the
 result with @ref YoriLibDereference.

 @param ArgC The number of arguments in the argument array.

 @param ArgV An array of YORI_STRINGs constituting the argument array.

 @param EncloseInQuotes If the argument contains a space, enclose it in quotes.
        If FALSE, return purely space delimited arguments.

 @param ApplyChildProcessEscapes If TRUE, quotes and backslashes preceeding
        quotes are escaped with an extra backslash.  If FALSE, this does not
        occur and the argument retains its original form.  Generally, this
        should be TRUE if the purpose of constructing the command line is to
        launch a child process, which is expected to process its command line
        and remove these escapes, and FALSE if the string is constructed to
        facilitate display or similar where the user specified the escapes to
        indicate how to display text and they should now be removed.

 @param CmdLine On successful completion, updated to point to a newly
        allocated string containing the entire command line.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOL
YoriLibBuildCmdlineFromArgcArgv(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[],
    __in BOOLEAN EncloseInQuotes,
    __in BOOLEAN ApplyChildProcessEscapes,
    __out PYORI_STRING CmdLine
    )
{
    DWORD count;
    PYORI_STRING ThisArg;
    DWORD BufferLength;
    DWORD SlashesToWrite;
    DWORD SrcOffset;
    DWORD SlashCount;
    DWORD DestOffset;
    DWORD CmdLineOffset;
    DWORD WriteSlashCount;
    BOOLEAN Quoted;
    BOOLEAN AddQuote;

    YoriLibInitEmptyString(CmdLine);

    BufferLength = 0;

    for (count = 0; count < ArgC; count++) {
        BufferLength += 1;
        ThisArg = &ArgV[count];
        Quoted = FALSE;
        if (EncloseInQuotes) {
            Quoted = YoriLibCheckIfArgNeedsQuotes(ThisArg);
            if (Quoted) {
                BufferLength += 2;
            }
        }
        for (SrcOffset = 0; SrcOffset < ThisArg->LengthInChars; SrcOffset++) {
            if (ApplyChildProcessEscapes &&
                (ThisArg->StartOfString[SrcOffset] == '\\' ||
                 ThisArg->StartOfString[SrcOffset] == '"')) {

                for (SlashCount = 0; SrcOffset + SlashCount < ThisArg->LengthInChars && ThisArg->StartOfString[SrcOffset + SlashCount] == '\\'; SlashCount++);
                if (SrcOffset + SlashCount < ThisArg->LengthInChars && ThisArg->StartOfString[SrcOffset + SlashCount] == '"') {
                    SlashesToWrite = SlashCount * 2 + 1;
                    SrcOffset += SlashCount;
                    BufferLength += 1;
                } else if (SrcOffset + SlashCount == ThisArg->LengthInChars && Quoted) {
                    SlashesToWrite = SlashCount * 2;
                    SrcOffset += SlashCount - 1;
                } else {
                    SlashesToWrite = SlashCount;
                    SrcOffset += SlashCount - 1;
                }
                BufferLength += SlashesToWrite;
            } else {
                BufferLength += 1;
            }
        }
    }

    BufferLength += 1;

    if (!YoriLibAllocateString(CmdLine, BufferLength)) {
        return FALSE;
    }

    CmdLineOffset = 0;
    for (count = 0; count < ArgC; count++) {
        ThisArg = &ArgV[count];

        if (count != 0) {
            CmdLine->StartOfString[CmdLineOffset] = ' ';
            CmdLineOffset++;
        }

        if (EncloseInQuotes) {
            Quoted = YoriLibCheckIfArgNeedsQuotes(ThisArg);

            if (Quoted) {
                CmdLine->StartOfString[CmdLineOffset] = '"';
                CmdLineOffset++;
            }
        } else {
            Quoted = FALSE;
        }

        for (SrcOffset = DestOffset = 0; SrcOffset < ThisArg->LengthInChars; SrcOffset++, DestOffset++) {
            if (ApplyChildProcessEscapes &&
                (ThisArg->StartOfString[SrcOffset] == '\\' ||
                 ThisArg->StartOfString[SrcOffset] == '"')) {

                for (SlashCount = 0; SrcOffset + SlashCount < ThisArg->LengthInChars && ThisArg->StartOfString[SrcOffset + SlashCount] == '\\'; SlashCount++);
                if (SrcOffset + SlashCount < ThisArg->LengthInChars && ThisArg->StartOfString[SrcOffset + SlashCount] == '"') {
                    // Escape the escapes and the quote
                    SlashesToWrite = SlashCount * 2 + 1;
                    AddQuote = TRUE;
                } else if (SrcOffset + SlashCount == ThisArg->LengthInChars && Quoted) {
                    // Escape the escapes but not the quote
                    SlashesToWrite = SlashCount * 2;
                    AddQuote = FALSE;
                } else {
                    // No escapes, just copy verbatim
                    SlashesToWrite = SlashCount;
                    AddQuote = FALSE;
                }
                for (WriteSlashCount = 0; WriteSlashCount < SlashesToWrite; WriteSlashCount++) {
                    CmdLine->StartOfString[CmdLineOffset + DestOffset + WriteSlashCount] = '\\';
                }
                if (AddQuote) {
                    CmdLine->StartOfString[CmdLineOffset + DestOffset + WriteSlashCount] = '"';
                    SrcOffset += SlashCount;
                    DestOffset += WriteSlashCount;
                } else {
                    SrcOffset += SlashCount - 1;
                    DestOffset += WriteSlashCount - 1;
                }
            } else {
                CmdLine->StartOfString[CmdLineOffset + DestOffset] = ThisArg->StartOfString[SrcOffset];
            }
        }
        CmdLineOffset += DestOffset;

        if (EncloseInQuotes) {
            if (Quoted) {
                CmdLine->StartOfString[CmdLineOffset] = '"';
                CmdLineOffset++;
            }
        }
    }

    CmdLine->StartOfString[CmdLineOffset] = '\0';
    CmdLine->LengthInChars = CmdLineOffset;

    return TRUE;
}

/**
 Expand any $ delimited variables by processing the input string and calling
 a callback function for every variable found, allowing the callback to
 populate the output with the correct value.

 @param String The input string, which may contain variables to expand.

 @param MatchChar The character to use to delimit the variable being expanded.

 @param PreserveEscapes If TRUE, escape characters (^) are preserved in the
        output; if FALSE, they are removed from the output.

 @param Function The callback function to invoke when variables are found.

 @param Context A caller provided context to pass to the callback function.

 @param ExpandedString A string allocated by this function containing the
        expanded result.  The caller should free this when it is no longer
        needed with @ref YoriLibFree .

 @return TRUE to indicate success, FALSE to indicate failure.
 */
__success(return)
BOOL
YoriLibExpandCommandVariables(
    __in PYORI_STRING String,
    __in TCHAR MatchChar,
    __in BOOLEAN PreserveEscapes,
    __in PYORILIB_VARIABLE_EXPAND_FN Function,
    __in_opt PVOID Context,
    __inout PYORI_STRING ExpandedString
    )
{
    DWORD DestIndex;
    DWORD Index;
    DWORD FinalIndex;
    BOOL Processed;
    YORI_STRING CmdString;
    YORI_STRING DestString;
    DWORD LengthNeeded;
    DWORD IgnoreUntil;

    if (ExpandedString->LengthAllocated < 256) {
        YoriLibFreeStringContents(ExpandedString);
        if (!YoriLibAllocateString(ExpandedString, 256)) {
            return FALSE;
        }
    }
    DestIndex = 0;
    IgnoreUntil = 0;

    for (Index = 0; Index < String->LengthInChars; Index++) {
        Processed = FALSE;

        if (Index >= IgnoreUntil && YoriLibIsEscapeChar(String->StartOfString[Index])) {
            IgnoreUntil = Index + 2;
            if (!PreserveEscapes) {
                continue;
            }
        }

        if (Index >= IgnoreUntil && String->StartOfString[Index] == MatchChar) {
            FinalIndex = Index + 1;
            while (FinalIndex < String->LengthInChars && String->StartOfString[FinalIndex] != MatchChar) {
                FinalIndex++;
            }

            YoriLibInitEmptyString(&CmdString);
            CmdString.StartOfString = &String->StartOfString[Index + 1];
            CmdString.LengthAllocated = 
            CmdString.LengthInChars = FinalIndex - Index - 1;

            while (TRUE) {
                YoriLibInitEmptyString(&DestString);
                DestString.StartOfString = &ExpandedString->StartOfString[DestIndex];
                DestString.LengthAllocated = ExpandedString->LengthAllocated - DestIndex - 1;

                LengthNeeded = Function(&DestString, &CmdString, Context);

                if (LengthNeeded <= (ExpandedString->LengthAllocated - DestIndex - 1)) {
                    Processed = TRUE;
                    DestIndex += LengthNeeded;
                    Index = FinalIndex;
                    break;
                } else {
                    ExpandedString->LengthInChars = DestIndex;
                    if (!YoriLibReallocateString(ExpandedString, ExpandedString->LengthAllocated * 4)) {
                        YoriLibFreeStringContents(ExpandedString);
                        return FALSE;
                    }
                }
            }
        }

        if (!Processed) {
            ExpandedString->StartOfString[DestIndex] = String->StartOfString[Index];
            DestIndex++;
        }

        if (DestIndex + 1 >= ExpandedString->LengthAllocated) {
            ExpandedString->LengthInChars = DestIndex;
            if (!YoriLibReallocateString(ExpandedString, ExpandedString->LengthAllocated * 4)) {
                YoriLibFreeStringContents(ExpandedString);
                return FALSE;
            }
        }
    }

    ExpandedString->LengthInChars = DestIndex;
    ExpandedString->StartOfString[DestIndex] = '\0';

    return TRUE;
}

/**
 Parses a NULL terminated command line string into an argument count and array
 of YORI_STRINGs corresponding to arguments.

 @param CmdLine The NULL terminated command line.

 @param MaxArgs The maximum number of arguments to return.  All trailing
        arguments are joined with the final argument.

 @param ApplyCaretAsEscape If TRUE, a caret character indicates the following
        character should be interpreted literally and should not be used to
        break arguments.  Caret characters are only meaningful within the
        shell, so external processes should generally use FALSE.  TRUE is
        used when parsing ArgC/ArgV to invoke builtin commands, where escapes
        need to be retained and removed later so the builtin can observe the
        escaped arguments.

 @param argc On successful completion, populated with the count of arguments.

 @return A pointer to an array of YORI_STRINGs containing the parsed
         arguments.
 */
PYORI_STRING
YoriLibCmdlineToArgcArgv(
    __in LPTSTR CmdLine,
    __in DWORD MaxArgs,
    __in BOOLEAN ApplyCaretAsEscape,
    __out PDWORD argc
    )
{
    DWORD ArgCount = 0;
    DWORD CharCount = 0;
    DWORD SlashCount;
    TCHAR BreakChar = ' ';
    TCHAR * c;
    PYORI_STRING ArgvArray;
    LPTSTR ReturnStrings;
    BOOLEAN EndArg;

    //
    //  Consume all spaces.  After this, we're either at
    //  the end of string, or we have an arg, and it
    //  might start with a quote
    //

    c = CmdLine;
    while (*c == ' ') c++;
    if (*c == '"') {
        BreakChar = '"';
        c++;
    }

    while (*c != '\0') {
        EndArg = FALSE;

        if (*c == '^' && c[1] != '\0' && ApplyCaretAsEscape) {
            c++;
            CharCount++;
        } else if (*c == '\\') {
            for (SlashCount = 1; c[SlashCount] == '\\'; SlashCount++);
            if (c[SlashCount] == '"') {

                //
                //  Because one char is left for regular processing, only
                //  adjust for one less pair.  Three slashes means consume
                //  two chars, output one; four means consume three, output
                //  one, etc.
                //

                if ((SlashCount % 2) == 0) {
                    SlashCount--;
                }
                CharCount += SlashCount / 2;
                c += SlashCount;
            }
        } else if (*c == BreakChar) {
            EndArg = TRUE;
        }

        if (ArgCount + 1 < MaxArgs && EndArg) {
            BreakChar = ' ';
            c++;
            while (*c == BreakChar) c++;
            if (*c == '"') {
                BreakChar = '"';
                c++;
            }
            ArgCount++;
        } else {
            CharCount++;

            //
            //  If we hit a break char, we count the argument then.
            //  If we hit end of string, count it here; note we're
            //  only counting it if we counted a character before it
            //  (ie., trailing whitespace is not an arg.)
            //

            c++;

            if (ArgCount < MaxArgs && *c == '\0') {
                ArgCount++;
            }
        }
    }

    *argc = ArgCount;
    if (ArgCount == 0) {
        return NULL;
    }

    ArgvArray = YoriLibReferencedMalloc( (ArgCount * sizeof(YORI_STRING)) + (CharCount + ArgCount) * sizeof(TCHAR));
    if (ArgvArray == NULL) {
        *argc = 0;
        return NULL;
    }

    ReturnStrings = (LPTSTR)(ArgvArray + ArgCount);

    ArgCount = 0;
    YoriLibInitEmptyString(&ArgvArray[ArgCount]);
    ArgvArray[ArgCount].StartOfString = ReturnStrings;
    ArgvArray[ArgCount].MemoryToFree = ArgvArray;
    YoriLibReference(ArgvArray);

    //
    //  Consume all spaces.  After this, we're either at
    //  the end of string, or we have an arg, and it
    //  might start with a quote
    //

    c = CmdLine;
    while (*c == ' ') c++;
    BreakChar = ' ';
    if (*c == '"') {
        BreakChar = '"';
        c++;
    }

    while (*c != '\0') {
        EndArg = FALSE;

        if (*c == '^' && c[1] != '\0' && ApplyCaretAsEscape) {
            *ReturnStrings = *c;
            ReturnStrings++;
            ArgvArray[ArgCount].LengthInChars++;
            c++;
        } else if (*c == '\\') {
            for (SlashCount = 1; c[SlashCount] == '\\'; SlashCount++);
            if (c[SlashCount] == '"') {

                //
                //  Always add one character in the regular path, below.  This
                //  code therefore needs to process each double-slash except
                //  the last one, and advance the c pointer to skip the first
                //  slash of the last pair.  After that can either be a slash
                //  or a double quote, which will be processed as a regular
                //  character below.
                //

                for (CharCount = 2; CharCount < SlashCount; CharCount += 2) {
                    *ReturnStrings = '\\';
                    ReturnStrings++;
                    ArgvArray[ArgCount].LengthInChars++;
                    c += 2;
                }
                c++;
            }
        } else if (*c == BreakChar) {
            EndArg = TRUE;
        }

        if (ArgCount + 1 < MaxArgs && EndArg) {
            *ReturnStrings = '\0';
            ReturnStrings++;
            ArgvArray[ArgCount].LengthAllocated = ArgvArray[ArgCount].LengthInChars + 1;

            BreakChar = ' ';
            c++;
            while (*c == BreakChar) c++;
            if (*c == '"') {
                BreakChar = '"';
                c++;
            }
            if (*c != '\0') {
                ArgCount++;
                YoriLibInitEmptyString(&ArgvArray[ArgCount]);
                ArgvArray[ArgCount].StartOfString = ReturnStrings;
                YoriLibReference(ArgvArray);
                ArgvArray[ArgCount].MemoryToFree = ArgvArray;
            }
        } else {
            *ReturnStrings = *c;
            ReturnStrings++;
            ArgvArray[ArgCount].LengthInChars++;

            //
            //  If we hit a break char, we count the argument then.
            //  If we hit end of string, count it here; note we're
            //  only counting it if we counted a character before it
            //  (ie., trailing whitespace is not an arg.)
            //

            c++;

            if (*c == '\0') {
                *ReturnStrings = '\0';
                ArgvArray[ArgCount].LengthAllocated = ArgvArray[ArgCount].LengthInChars + 1;
            }
        }
    }

    return ArgvArray;
}


// vim:sw=4:ts=4:et:


<|start_filename|>sdir/utils.c<|end_filename|>
/**
 * @file sdir/utils.c
 *
 * Colorful, sorted and optionally rich directory enumeration
 * for Windows.
 *
 * This module contains helper functions that can be used in a variety of
 * situations.
 *
 * Copyright (c) 2014-2018 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "sdir.h"

//
//  These are general helper routines to assist in converting strings into
//  something we can use directly.
//

/**
 Parse a NULL terminated string and return a 32 bit unsigned integer from
 the result.  This function can parse prefixed hex or decimal inputs, and
 indicates the end of the string that it processed (meaning, the first non
 numeric character in the string.)

 @param str Pointer to the string to parse.

 @param endptr Optionally points to a value to receive a pointer to the first
        non-numeric character in the string.

 @return The parsed, 32 bit unsigned integer value from the string.
 */
DWORD
SdirStringToNum32(
    __in LPCTSTR str,
    __out_opt LPCTSTR* endptr
    )
{
    DWORD ret = 0;
    if (str[0] == '0' && str[1] == 'x') {
        str += 2;
        while ((*str >= '0' && *str <= '9') || (*str >= 'a' && *str <= 'f') || (*str >= 'A' && *str <= 'F')) {
            ret *= 16;
            if (*str >= '0' && *str <= '9') {
                ret += *str - '0';
            } else if (*str >= 'a' && *str <= 'f') {
                ret += *str - 'a' + 10;
            } else if (*str >= 'A' && *str <= 'F') {
                ret += *str - 'A' + 10;
            }
            str++;
        }
    } else {
        while (*str >= '0' && *str <= '9') {
            ret *= 10;
            ret += *str - '0';
            str++;
        }
    }
    if (endptr) {
        *endptr = str;
    }
    return ret;
}

//
//  Formatting support.  Generic routines used across different types of data.
//

/**
 Populate a file size value into a string of formatted characters.  This
 function will apply any file size suffixes to the string as needed.  It
 will only write 6 chars to Buffer (space, 4 chars of number possibly
 including a decimal point, and suffix.)

 @param Buffer The buffer to populate with the file size.

 @param Attributes The color attributes to apply to the resulting file size.

 @param GenericSize Pointer to the file size as a number.

 @return The number of characters written to the buffer (6.)
 */
ULONG
SdirDisplayGenericSize(
    __out_ecount(6) PSDIR_FMTCHAR Buffer,
    __in YORILIB_COLOR_ATTRIBUTES Attributes,
    __in PLARGE_INTEGER GenericSize
    )
{
    TCHAR StrSize[10];
    YORI_STRING YsSize;

    if (Buffer) {
        YoriLibInitEmptyString(&YsSize);
        YsSize.StartOfString = &StrSize[1];
        YsSize.LengthAllocated = sizeof(StrSize)/sizeof(StrSize[0]) - 1;

        YoriLibFileSizeToString(&YsSize, GenericSize);
        ASSERT(YsSize.LengthInChars == 5);

        StrSize[0] = ' ';
        SdirPasteStr(Buffer, StrSize, Attributes, 6);
    }
    return 6;
}

/**
 Populate a generic 64 bit number into a string using formatted hex
 characters.  It will only write 18 chars to Buffer (space, 8 chars of hex,
 seperator, 8 chars of hex.)

 @param Buffer The buffer to populate with the hex string.

 @param Attributes The color attributes to apply to the resulting string.

 @param Hex Pointer to the input value as a number.

 @return The number of characters written to the buffer (18.)
 */
ULONG
SdirDisplayHex64 (
    __out_ecount(18) PSDIR_FMTCHAR Buffer,
    __in YORILIB_COLOR_ATTRIBUTES Attributes,
    __in PLARGE_INTEGER Hex
    )
{
    TCHAR Str[19];

    if (Buffer) {
        YoriLibSPrintfS(Str, sizeof(Str)/sizeof(Str[0]), _T(" %08x`%08x"), (int)Hex->HighPart, (int)Hex->LowPart);
        SdirPasteStr(Buffer, Str, Attributes, 18);
    }

    return 18;
}

/**
 Populate a generic 32 bit number into a string using formatted hex
 characters.  It will only write 9 chars to Buffer (space, 8 chars of hex.)

 @param Buffer The buffer to populate with the hex string.

 @param Attributes The color attributes to apply to the resulting string.

 @param Hex Pointer to the input value as a number.

 @return The number of characters written to the buffer (9.)
 */
ULONG
SdirDisplayHex32 (
    __out_ecount(9) PSDIR_FMTCHAR Buffer,
    __in YORILIB_COLOR_ATTRIBUTES Attributes,
    __in DWORD Hex
    )
{
    TCHAR Str[10];

    if (Buffer) {
        YoriLibSPrintfS(Str, sizeof(Str)/sizeof(Str[0]), _T(" %08x"), (int)Hex);
        SdirPasteStr(Buffer, Str, Attributes, 9);
    }
    return 9;
}

/**
 Populate a generic buffer into a string using formatted hex characters.
 It will write two formatted chars per char in the input buffer, plus a
 space.

 @param Buffer The buffer to populate with the hex string.

 @param Attributes The color attributes to apply to the resulting string.

 @param InputBuffer Pointer to the buffer containing chars to encode into
        hex.  Note this is always an 8 bit string.

 @param Size The number of bytes in InputBuffer to encode into Buffer.

 @return The number of characters written to the buffer.
 */
ULONG
SdirDisplayGenericHexBuffer (
    __out_ecount(Size * 2 + 1) PSDIR_FMTCHAR Buffer,
    __in YORILIB_COLOR_ATTRIBUTES Attributes,
    __in PUCHAR InputBuffer,
    __in DWORD Size
    )
{
    TCHAR Str[3];
    DWORD Offset;

    if (Buffer) {
        SdirPasteStr(Buffer, _T(" "), Attributes, 1);
        for (Offset = 0; Offset < Size; Offset++) {
            YoriLibSPrintfS(Str, sizeof(Str)/sizeof(Str[0]), _T("%02x"), InputBuffer[Offset]);
            SdirPasteStr(Buffer + 1 + 2 * Offset, Str, Attributes, 2);
        }
    }

    return Size * 2 + 1;
}

/**
 Populate a date into a formatted string.  It will write 11 formatted chars
 into the buffer (space, 4 chars for year, seperator, 2 chars for month,
 seperator, 2 chars for day.)

 @param Buffer The buffer to populate with the date string.

 @param Attributes The color attributes to apply to the resulting string.

 @param Time Pointer to the timestamp specifying the date.  Note only the
        date components are consumed, time values are ignored.

 @return The number of characters written to the buffer (11.)
 */
ULONG
SdirDisplayFileDate (
    __out_ecount(11) PSDIR_FMTCHAR Buffer,
    __in YORILIB_COLOR_ATTRIBUTES Attributes,
    __in SYSTEMTIME * Time
    )
{
    TCHAR StrDate[16];

    if (Buffer) {
        YoriLibSPrintfS(StrDate,
                    sizeof(StrDate)/sizeof(StrDate[0]),
                    _T(" %04i/%02i/%02i"),
                    Time->wYear,
                    Time->wMonth,
                    Time->wDay);

        SdirPasteStr(Buffer, StrDate, Attributes, 11);
    }
    return 11;
}

/**
 Populate a time into a formatted string.  It will write 9 formatted chars
 into the buffer (space, 2 chars for hour, seperator, 2 chars for min,
 seperator, 2 chars for second.)

 @param Buffer The buffer to populate with the time string.

 @param Attributes The color attributes to apply to the resulting string.

 @param Time Pointer to the timestamp specifying the time.  Note only the
        time components are consumed, date values are ignored.

 @return The number of characters written to the buffer (9.)
 */
ULONG
SdirDisplayFileTime (
    __out_ecount(9) PSDIR_FMTCHAR Buffer,
    __in YORILIB_COLOR_ATTRIBUTES Attributes,
    __in SYSTEMTIME * Time
    )
{
    TCHAR StrTime[10];

    if (Buffer) {
        YoriLibSPrintfS(StrTime,
                    sizeof(StrTime)/sizeof(StrTime[0]),
                    _T(" %02i:%02i:%02i"),
                    Time->wHour,
                    Time->wMinute,
                    Time->wSecond);

        SdirPasteStr(Buffer, StrTime, Attributes, 9);
    }
    return 9;
}

/**
 Multiply a 64 bit value by a 32 bit value which consists of only a single
 set bit (ie., power of two.)  This is done to allow limited 64 bit math
 in environments that don't natively support 64 bit math by using bit
 shift operations.

 @param OriginalValue The value to multiply.

 @param MultiplyBy A value to multiply by, which must consist of a power of
        two value.

 @return The resulting multiplied value.
 */
LARGE_INTEGER
SdirMultiplyViaShift(
    __in LARGE_INTEGER OriginalValue,
    __in DWORD MultiplyBy
    )
{
    DWORD ShiftCount = 0;
    DWORD ShiftedMultiplyBy = MultiplyBy;
    LARGE_INTEGER ReturnValue;

    if (MultiplyBy == 0) {
        ReturnValue.HighPart = 0;
        ReturnValue.LowPart = 0;
        return ReturnValue;
    }

    while ((ShiftedMultiplyBy & 1) == 0) {
        ShiftedMultiplyBy = ShiftedMultiplyBy >> 1;
        ShiftCount++;
    }

    ReturnValue.HighPart = OriginalValue.HighPart << ShiftCount;
    if (ShiftCount > 0) {
       ReturnValue.HighPart += OriginalValue.LowPart >> (32 - ShiftCount);
    }
    ReturnValue.LowPart = OriginalValue.LowPart << ShiftCount;

    return ReturnValue;
}

/**
 Update summary information for free and total disk space from a path using
 GetDiskFreeSpace.  This is a fallback path when GetDiskFreeSpaceEx is not
 available.  Because GetDiskFreeSpace relies on the caller doing 64 bit math,
 this is emulated using @ref SdirMultiplyViaShift .

 @param Path Pointer to a string specifying the path to query free disk space
        for.

 @param LocalSummary Pointer to the summary information to update with free
        disk space information.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SdirPopulateSummaryWithGetDiskFreeSpace(
    __in LPTSTR Path,
    __inout PSDIR_SUMMARY LocalSummary
    )
{
    LARGE_INTEGER Temp;
    ULONG BytesPerSector;
    ULONG SectorsPerCluster;
    ULONG FreeClusters;
    ULONG TotalClusters;

    //
    //  This path requires 64 bit multiplication which is emulated using
    //  shifts since we can't rely on CRT support for it.
    //

    if (GetDiskFreeSpace(Path, &SectorsPerCluster, &BytesPerSector, &FreeClusters, &TotalClusters)) {
        Temp.HighPart = 0;
        Temp.LowPart = TotalClusters;

        LocalSummary->VolumeSize = SdirMultiplyViaShift(SdirMultiplyViaShift(Temp, SectorsPerCluster), BytesPerSector);

        Temp.HighPart = 0;
        Temp.LowPart = FreeClusters;
        LocalSummary->FreeSize = SdirMultiplyViaShift(SdirMultiplyViaShift(Temp, SectorsPerCluster), BytesPerSector);
        return TRUE;
    }
    return FALSE;
}

// vim:sw=4:ts=4:et:


<|start_filename|>rmdir/rmdir.c<|end_filename|>
/**
 * @file rmdir/rmdir.c
 *
 * Yori shell rmdir
 *
 * Copyright (c) 2017-2021 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS RMDIR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE RMDIR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 Help text to display to the user.
 */
const
CHAR strRmdirHelpText[] =
        "\n"
        "Removes directories.\n"
        "\n"
        "RMDIR [-license] [-b] [-r] [-s] <dir> [<dir>...]\n"
        "\n"
        "   -b             Use basic search criteria for directories only\n"
        "   -f             Delete files as well as directories\n"
        "   -l             Delete links without contents\n"
        "   -p             Delete with POSIX semantics\n"
        "   -r             Send directories to the recycle bin\n"
        "   -s             Remove all contents of each directory\n";

/**
 Display usage text to the user.
 */
BOOL
RmdirHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Rmdir %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strRmdirHelpText);
    return TRUE;
}

/**
 Context information when files are found.
 */
typedef struct _RMDIR_CONTEXT {

    /**
     If TRUE, objects should be sent to the recycle bin rather than directly
     deleted.
     */
    BOOLEAN RecycleBin;

    /**
     If TRUE, delete files as well as directories.
     */
    BOOLEAN DeleteFiles;

    /**
     If TRUE, delete with POSIX semantics.
     */
    BOOLEAN PosixSemantics;

    /**
     The number of directories successfully removed.
     */
    DWORD DirectoriesRemoved;

} RMDIR_CONTEXT, *PRMDIR_CONTEXT;

BOOL
RmdirFileEnumerateErrorCallback(
    __in PYORI_STRING FilePath,
    __in DWORD ErrorCode,
    __in DWORD Depth,
    __in PVOID Context
    );

/**
 A callback that is invoked when a file is found that matches a search criteria
 specified in the set of strings to enumerate.

 @param FilePath Pointer to the file path that was found.

 @param FileInfo Information about the file.

 @param Depth Specifies the recursion depth.  Ignored in this application.

 @param Context Pointer to a RMDIR_CONTEXT.

 @return TRUE to continute enumerating, FALSE to abort.
 */
BOOL
RmdirFileFoundCallback(
    __in PYORI_STRING FilePath,
    __in PWIN32_FIND_DATA FileInfo,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    DWORD Err = NO_ERROR;
    LPTSTR ErrText;
    DWORD OldAttributes;
    DWORD NewAttributes;
    BOOL FileDeleted;
    PRMDIR_CONTEXT RmdirContext = (PRMDIR_CONTEXT)Context;

    //
    //  Don't delete any files that are specified on the command line
    //  directly.  These can be deleted if they're enumerated underneath
    //  a parent object.
    //

    if ((FileInfo->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0 &&
        Depth == 0 &&
        !RmdirContext->DeleteFiles) {

        RmdirFileEnumerateErrorCallback(FilePath, ERROR_DIRECTORY, Depth, Context);
        return TRUE;
    }

    ASSERT(YoriLibIsStringNullTerminated(FilePath));

    FileDeleted = FALSE;

    //
    //  Try to delete it.
    //

    if (RmdirContext->RecycleBin) {
        if (YoriLibRecycleBinFile(FilePath)) {
            if ((FileInfo->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
                RmdirContext->DirectoriesRemoved++;
            }
            FileDeleted = TRUE;
        }
    }

    if (!FileDeleted) {
        if (RmdirContext->PosixSemantics) {
            if (!YoriLibPosixDeleteFile(FilePath)) {
                Err = GetLastError();
            } else if ((FileInfo->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
                RmdirContext->DirectoriesRemoved++;
            }
        } else if ((FileInfo->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) {
            if (!DeleteFile(FilePath->StartOfString)) {
                Err = GetLastError();
            }
        } else {
            if (!RemoveDirectory(FilePath->StartOfString)) {
                Err = GetLastError();
            } else {
                RmdirContext->DirectoriesRemoved++;
            }
        }
    }


    //
    //  If it fails with access denied, try to remove any readonly, hidden or
    //  system attributes which might be getting in the way, then try the
    //  delete again.
    //

    if (Err == ERROR_ACCESS_DENIED) {

        OldAttributes = GetFileAttributes(FilePath->StartOfString);
        NewAttributes = OldAttributes & ~(FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM);

        if (OldAttributes != NewAttributes) {
            SetFileAttributes(FilePath->StartOfString, NewAttributes);

            Err = NO_ERROR;

            if ((FileInfo->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) {
                if (!DeleteFile(FilePath->StartOfString)) {
                    Err = GetLastError();
                }
            } else {
                if (!RemoveDirectory(FilePath->StartOfString)) {
                    Err = GetLastError();
                } else {
                    RmdirContext->DirectoriesRemoved++;
                }
            }

            if (Err != NO_ERROR) {
                SetFileAttributes(FilePath->StartOfString, OldAttributes);
            }
        }
    }

    //
    //  If we still can't delete it, return an error.
    //

    if (Err != NO_ERROR) {
        ErrText = YoriLibGetWinErrorText(Err);
        if ((FileInfo->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("rmdir: delete failed: %y: %s"), FilePath, ErrText);
        } else {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("rmdir: rmdir failed: %y: %s"), FilePath, ErrText);
        }
        YoriLibFreeWinErrorText(ErrText);
    }
    return TRUE;
}

/**
 A callback that is invoked when a directory cannot be successfully enumerated.

 @param FilePath Pointer to the file path that could not be enumerated.

 @param ErrorCode The Win32 error code describing the failure.

 @param Depth Recursion depth, ignored in this application.

 @param Context Context, ignored in this function.

 @return TRUE to continute enumerating, FALSE to abort.
 */
BOOL
RmdirFileEnumerateErrorCallback(
    __in PYORI_STRING FilePath,
    __in DWORD ErrorCode,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    YORI_STRING UnescapedFilePath;
    BOOL Result = FALSE;

    UNREFERENCED_PARAMETER(Depth);
    UNREFERENCED_PARAMETER(Context);

    YoriLibInitEmptyString(&UnescapedFilePath);
    if (!YoriLibUnescapePath(FilePath, &UnescapedFilePath)) {
        UnescapedFilePath.StartOfString = FilePath->StartOfString;
        UnescapedFilePath.LengthInChars = FilePath->LengthInChars;
    }

    if (ErrorCode == ERROR_FILE_NOT_FOUND || ErrorCode == ERROR_PATH_NOT_FOUND) {
        if (Depth == 0) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("File or directory not found: %y\n"), &UnescapedFilePath);
        }
        Result = TRUE;
    } else {
        LPTSTR ErrText = YoriLibGetWinErrorText(ErrorCode);
        YORI_STRING DirName;
        LPTSTR FilePart;
        YoriLibInitEmptyString(&DirName);
        DirName.StartOfString = UnescapedFilePath.StartOfString;
        FilePart = YoriLibFindRightMostCharacter(&UnescapedFilePath, '\\');
        if (FilePart != NULL && ErrorCode != ERROR_DIRECTORY) {
            DirName.LengthInChars = (DWORD)(FilePart - DirName.StartOfString);
        } else {
            DirName.LengthInChars = UnescapedFilePath.LengthInChars;
        }
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Enumerate of %y failed: %s"), &DirName, ErrText);
        YoriLibFreeWinErrorText(ErrText);
    }
    YoriLibFreeStringContents(&UnescapedFilePath);
    return Result;
}


#ifdef YORI_BUILTIN
/**
 The main entrypoint for the rmdir builtin command.
 */
#define ENTRYPOINT YoriCmd_YRMDIR
#else
/**
 The main entrypoint for the rmdir standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the rmdir cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the process indicating success or failure.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    BOOL Recursive;
    BOOL BasicEnumeration;
    BOOL DeleteLinks;
    DWORD MatchFlags;
    DWORD StartArg = 0;
    DWORD i;
    RMDIR_CONTEXT RmdirContext;
    YORI_STRING Arg;

    ZeroMemory(&RmdirContext, sizeof(RmdirContext));

    Recursive = FALSE;
    BasicEnumeration = FALSE;
    DeleteLinks = FALSE;

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                RmdirHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2017-2021"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("b")) == 0) {
                BasicEnumeration = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("f")) == 0) {
                ArgumentUnderstood = TRUE;
                RmdirContext.DeleteFiles = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("l")) == 0) {
                DeleteLinks = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("p")) == 0) {
                ArgumentUnderstood = TRUE;
                RmdirContext.PosixSemantics = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("q")) == 0) {
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("r")) == 0) {
                ArgumentUnderstood = TRUE;
                RmdirContext.RecycleBin = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("s")) == 0) {
                Recursive = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("s/q")) == 0) {
                Recursive = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                StartArg = i + 1;
                ArgumentUnderstood = TRUE;
                break;
            }

        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    if (StartArg == 0 || StartArg == ArgC) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("rmdir: missing argument\n"));
        return EXIT_FAILURE;
    }

    if (RmdirContext.PosixSemantics &&
        DllKernel32.pSetFileInformationByHandle == NULL) {

        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("rmdir: OS support not present\n"));
        return EXIT_FAILURE;
    }

    MatchFlags = YORILIB_FILEENUM_RETURN_DIRECTORIES;
    if (RmdirContext.DeleteFiles) {
        MatchFlags |= YORILIB_FILEENUM_RETURN_FILES;
    }
    if (Recursive) {
        MatchFlags |= YORILIB_FILEENUM_RECURSE_BEFORE_RETURN | YORILIB_FILEENUM_RETURN_FILES;
    }
    if (BasicEnumeration) {
        MatchFlags |= YORILIB_FILEENUM_BASIC_EXPANSION;
    }
    if (DeleteLinks) {
        MatchFlags |= YORILIB_FILEENUM_NO_LINK_TRAVERSE;
    }

    for (i = StartArg; i < ArgC; i++) {
        YoriLibForEachFile(&ArgV[i],
                           MatchFlags,
                           0,
                           RmdirFileFoundCallback,
                           RmdirFileEnumerateErrorCallback,
                           &RmdirContext);
    }

    if (RmdirContext.DirectoriesRemoved == 0) {
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>sync/sync.c<|end_filename|>
/**
 * @file sync/sync.c
 *
 * Yori create files or update timestamps
 *
 * Copyright (c) 2018 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 Help text to display to the user.
 */
const
CHAR strSyncHelpText[] =
        "\n"
        "Flush files, directories or volumes to disk.\n"
        "\n"
        "SYNC [-license] [-b] [-s] [-v] <file>...\n"
        "\n"
        "   -b             Use basic search criteria for files only\n"
        "   -q             Query if the volume is in use, and flush if it is not in use\n"
        "   -r             Dismount and remount the volume\n"
        "   -s             Process files from all subdirectories\n"
        "   -v             Display verbose output\n";

/**
 Display usage text to the user.
 */
BOOL
SyncHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Sync %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strSyncHelpText);
    return TRUE;
}

/**
 Context passed to the callback which is invoked for each file found.
 */
typedef struct _SYNC_CONTEXT {

    /**
     Counts the number of files processed in an enumerate.  If this is zero,
     the program assumes the request is to create a new file.
     */
    DWORD FilesFoundThisArg;

    /**
     TRUE if the volume should be locked.  In Windows, this will fail if the
     volume is in use, otherwise it will flush the volume ready for removal.
     */
    BOOL LockVolume;

    /**
     TRUE if the volume should be dismounted.  In Windows, this will probably
     be remounted again immediately, hence externally calling this a remount
     command.
     */
    BOOL VolumeDismount;

    /**
     If TRUE, display output for each object where sync is attempted.
     */
    BOOL Verbose;

} SYNC_CONTEXT, *PSYNC_CONTEXT;

/**
 A callback that is invoked when a file is found that matches a search criteria
 specified in the set of strings to enumerate.

 @param FilePath Pointer to the file path that was found.

 @param FileInfo Information about the file.  Note in this application this can
        be NULL when it is operating on files that do not yet exist.

 @param Depth Specifies the recursion depth.  Ignored in this application.

 @param Context Pointer to the sync context structure indicating the
        action to perform and populated with the file and line count found.

 @return TRUE to continute enumerating, FALSE to abort.
 */
BOOL
SyncFileFoundCallback(
    __in PYORI_STRING FilePath,
    __in_opt PWIN32_FIND_DATA FileInfo,
    __in DWORD Depth,
    __in PVOID Context
    )
{
    HANDLE FileHandle;
    DWORD DesiredAccess;
    DWORD LastError;
    DWORD BytesReturned;
    LPTSTR ErrText;
    PSYNC_CONTEXT SyncContext = (PSYNC_CONTEXT)Context;

    UNREFERENCED_PARAMETER(Depth);
    UNREFERENCED_PARAMETER(FileInfo);

    ASSERT(YoriLibIsStringNullTerminated(FilePath));

    SyncContext->FilesFoundThisArg++;

    DesiredAccess = GENERIC_WRITE;

    //
    //  If the user requested a volume operation, find the volume name and
    //  attempt the operation on the volume.  If not, just use the file name
    //  that has already been located.
    //

    if (SyncContext->VolumeDismount || SyncContext->LockVolume) {
        YORI_STRING VolumePath;
        YoriLibInitEmptyString(&VolumePath);
        if (!YoriLibGetVolumePathName(FilePath, &VolumePath)) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("sync: could not determine volume for %y\n"), FilePath);
            return TRUE;
        }

        if (SyncContext->Verbose) {
            if (SyncContext->VolumeDismount) {
                YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("sync: dismounting %y\n"), &VolumePath);
            } else {
                YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("sync: locking %y\n"), &VolumePath);
            }
        }

        FileHandle = CreateFile(VolumePath.StartOfString,
                                DesiredAccess,
                                FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                                NULL,
                                OPEN_EXISTING,
                                FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS,
                                NULL);

        if (FileHandle == NULL || FileHandle == INVALID_HANDLE_VALUE) {
            LastError = GetLastError();
            ErrText = YoriLibGetWinErrorText(LastError);
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("sync: open of %y failed: %s"), &VolumePath, ErrText);
            YoriLibFreeWinErrorText(ErrText);
            YoriLibFreeStringContents(&VolumePath);
            return TRUE;
        }

        if (SyncContext->VolumeDismount) {
            if (!DeviceIoControl(FileHandle, FSCTL_DISMOUNT_VOLUME, NULL, 0, NULL, 0, &BytesReturned, NULL)) {
                LastError = GetLastError();
                ErrText = YoriLibGetWinErrorText(LastError);
                YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("sync: dismount of %y failed: %s"), &VolumePath, ErrText);
                YoriLibFreeWinErrorText(ErrText);
                YoriLibFreeStringContents(&VolumePath);
                return TRUE;
            }
        } else {
            if (!DeviceIoControl(FileHandle, FSCTL_LOCK_VOLUME, NULL, 0, NULL, 0, &BytesReturned, NULL)) {
                LastError = GetLastError();
                ErrText = YoriLibGetWinErrorText(LastError);
                YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("sync: lock of %y failed, volume may be in use: %s"), &VolumePath, ErrText);
                YoriLibFreeWinErrorText(ErrText);
                YoriLibFreeStringContents(&VolumePath);
                return TRUE;
            }
        }

        YoriLibFreeStringContents(&VolumePath);
        CloseHandle(FileHandle);
        return TRUE;

    } else {

        if (SyncContext->Verbose) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("sync: syncing %y\n"), FilePath);
        }

        FileHandle = CreateFile(FilePath->StartOfString,
                                DesiredAccess,
                                FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                                NULL,
                                OPEN_ALWAYS,
                                FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS,
                                NULL);

        if (FileHandle == NULL || FileHandle == INVALID_HANDLE_VALUE) {
            LastError = GetLastError();
            ErrText = YoriLibGetWinErrorText(LastError);
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("sync: open of %y failed: %s"), FilePath, ErrText);
            YoriLibFreeWinErrorText(ErrText);
            return TRUE;
        }

        if (!FlushFileBuffers(FileHandle)) {
            LastError = GetLastError();
            ErrText = YoriLibGetWinErrorText(LastError);
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("sync: flush of %y failed: %s"), FilePath, ErrText);
            YoriLibFreeWinErrorText(ErrText);
            return TRUE;
        }

        CloseHandle(FileHandle);
        return TRUE;
    }
}

#ifdef YORI_BUILTIN
/**
 The main entrypoint for the sync builtin command.
 */
#define ENTRYPOINT YoriCmd_SYNC
#else
/**
 The main entrypoint for the sync standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the sync cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the child process on success, or failure if the child
         could not be launched.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    BOOL ArgumentUnderstood;
    DWORD i;
    DWORD StartArg = 0;
    DWORD MatchFlags;
    BOOL Recursive = FALSE;
    BOOL BasicEnumeration = FALSE;
    SYNC_CONTEXT SyncContext;
    YORI_STRING Arg;

    ZeroMemory(&SyncContext, sizeof(SyncContext));

    for (i = 1; i < ArgC; i++) {

        ArgumentUnderstood = FALSE;
        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                SyncHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2018"));
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("b")) == 0) {
                BasicEnumeration = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("q")) == 0) {
                SyncContext.LockVolume = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("r")) == 0) {
                SyncContext.VolumeDismount = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("s")) == 0) {
                Recursive = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("v")) == 0) {
                SyncContext.Verbose = TRUE;
                ArgumentUnderstood = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("-")) == 0) {
                StartArg = i + 1;
                ArgumentUnderstood = TRUE;
                break;
            }
        } else {
            ArgumentUnderstood = TRUE;
            StartArg = i;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), &ArgV[i]);
        }
    }

    //
    //  Attempt to enable backup privilege so an administrator can access more
    //  objects successfully.
    //

    YoriLibEnableBackupPrivilege();

    //
    //  If no file name is specified, we have nothing to flush.
    //

    if (StartArg == 0 || StartArg == ArgC) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("sync: missing argument\n"));
        return EXIT_FAILURE;
    } else {
        MatchFlags = YORILIB_FILEENUM_RETURN_FILES | YORILIB_FILEENUM_RETURN_DIRECTORIES;
        if (Recursive) {
            MatchFlags |= YORILIB_FILEENUM_RECURSE_BEFORE_RETURN | YORILIB_FILEENUM_RECURSE_PRESERVE_WILD;
        }
        if (BasicEnumeration) {
            MatchFlags |= YORILIB_FILEENUM_BASIC_EXPANSION;
        }

        for (i = StartArg; i < ArgC; i++) {

            SyncContext.FilesFoundThisArg = 0;
            YoriLibForEachStream(&ArgV[i], MatchFlags, 0, SyncFileFoundCallback, NULL, &SyncContext);
            if (SyncContext.FilesFoundThisArg == 0) {
                YORI_STRING FullPath;
                YoriLibInitEmptyString(&FullPath);
                if (YoriLibUserStringToSingleFilePath(&ArgV[i], TRUE, &FullPath)) {
                    SyncFileFoundCallback(&FullPath, NULL, 0, &SyncContext);
                    YoriLibFreeStringContents(&FullPath);
                }
            }
        }
    }

    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>yui/Makefile<|end_filename|>

BINARIES=yui.exe yuishook.dll

!INCLUDE "..\config\common.mk"
YORI_BIN_VER_MAJOR=$(YORI_VER_MAJOR)
YORI_BIN_VER_MINOR=$(YORI_VER_MINOR)

!IF $(PDB)==1
LINKPDB=/Pdb:yui.pdb
LINKPDB_YUISHOOK=/Pdb:yuishook.pdb
!ENDIF

LDFLAGS_NT4UP=$(LDFLAGS:3.10=4.0)
LIBS=$(LIBS) user32.lib gdi32.lib comdlg32.lib

BIN_OBJS=\
	 menu.obj        \
	 resource.obj    \
	 taskbar.obj     \
	 yui.obj         \

YUISHOOK_OBJS=\
	 yuishook.obj    \

compile: $(BIN_OBJS) $(YUISHOOK_OBJS)

yui.exe: $(BIN_OBJS) $(YORILIB) $(YORICRT)
	@echo $@
	@$(LINK) $(LDFLAGS_NT4UP) -entry:$(YENTRY) $(BIN_OBJS) $(LIBS) $(CRTLIB) $(YORILIB) -version:$(YORI_VER_MAJOR).$(YORI_VER_MINOR) $(LINKPDB) -out:$@

yuishook.dll: $(YUISHOOK_OBJS) $(YORIVER)
	@echo $@
	@$(LINK) $(LDFLAGS) -dll -def:yuishook.def $(YUISHOOK_OBJS) $(LIBS) $(CRTLIB) $(YORILIB) $(YORIVER) -version:$(YORI_VER_MAJOR).$(YORI_VER_MINOR) $(LINKPDB_YUISHOOK) -out:$@



<|start_filename|>sdir/init.c<|end_filename|>
/**
 * @file sdir/init.c
 *
 * Colorful, sorted and optionally rich directory enumeration
 * for Windows.
 *
 * This module implements initialization support including argument parsing
 * and initializing default options.
 *
 * Copyright (c) 2014-2018 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "sdir.h"

/**
 Invoked when the user presses Ctrl+C or similar during program execution.

 @param dwHandlerType The type of event that the user initiated.

 @return TRUE to terminate processing of further handlers, FALSE to call the
         next handler in the chain.
 */
BOOL WINAPI
SdirCancelHandler (
    __in DWORD dwHandlerType
    )
{
    UNREFERENCED_PARAMETER(dwHandlerType);
    Opts->Cancelled = TRUE;
    return TRUE;
}

/**
 Initialize global application state.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SdirAppInitialize(VOID)
{
    CONSOLE_SCREEN_BUFFER_INFO ScreenInfo;
    HANDLE hConsoleOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    DWORD CurrentMode;
    DWORD MajorVersion;
    DWORD MinorVersion;
    DWORD BuildNumber;
    BOOL SupportsAutoLineWrap;
    BOOL SupportsExtendedChars;

    Opts = YoriLibMalloc(sizeof(SDIR_OPTS));
    if (Opts == NULL) {
        return FALSE;
    }
    ZeroMemory(Opts, sizeof(SDIR_OPTS));

    Summary = YoriLibMalloc(sizeof(SDIR_SUMMARY));
    if (Summary == NULL) {
        YoriLibFree(Opts);
        Opts = NULL;
        return FALSE;
    }
    ZeroMemory(Summary, sizeof(SDIR_SUMMARY));

    //
    //  For simplicity, initialize this now.  On failure we restore to
    //  this value.  Hopefully we'll find the correct value before any
    //  failure can occur.
    //

    SdirDefaultColor.Win32Attr = (UCHAR)YoriLibVtGetDefaultColor();
    Opts->PreviousAttributes.Ctrl = SdirDefaultColor.Ctrl;
    Opts->PreviousAttributes.Win32Attr = SdirDefaultColor.Win32Attr;

    //
    //  Check if we're talking to a console or some other kind of object
    //  (file, pipe, etc.)
    //

    Opts->OutputExtendedCharacters = FALSE;
    Opts->OutputHasAutoLineWrap = FALSE;

    if (YoriLibQueryConsoleCapabilities(hConsoleOutput, NULL, &SupportsExtendedChars, &SupportsAutoLineWrap)) {
        if (SupportsExtendedChars) {
            Opts->OutputExtendedCharacters = TRUE;
        }
        if (SupportsAutoLineWrap) {
            Opts->OutputHasAutoLineWrap = TRUE;
        }
    }

    if (!GetConsoleMode(hConsoleOutput, &CurrentMode)) {
        Opts->OutputWithConsoleApi = FALSE;
    } else {
        Opts->OutputWithConsoleApi = TRUE;
        Opts->EnablePause = TRUE;
    }

    //
    //  Try to determine the console width so we can size columns.  We
    //  try to use the window size, not the buffer size.
    //  If we're not talking to a console, this might fail, and we have
    //  to fall back to some kind of default.
    //

    if (!GetConsoleScreenBufferInfo(hConsoleOutput, &ScreenInfo)) {
        if (!YoriLibGetWindowDimensions(hConsoleOutput, &Opts->ConsoleWidth, &Opts->ConsoleHeight)) {
            SdirDisplayError(GetLastError(), _T("GetConsoleScreenBufferInfo"));
            return FALSE;
        }
        Opts->ConsoleBufferWidth = Opts->ConsoleWidth;
    } else {
        Opts->ConsoleBufferWidth = 
        Opts->ConsoleWidth = ScreenInfo.dwSize.X;

        if (Opts->ConsoleWidth > (DWORD)(ScreenInfo.srWindow.Right - ScreenInfo.srWindow.Left + 1)) {
            Opts->ConsoleWidth = (DWORD)(ScreenInfo.srWindow.Right - ScreenInfo.srWindow.Left + 1);
        }

        Opts->ConsoleHeight = ScreenInfo.dwSize.Y;
        if (Opts->ConsoleHeight > (DWORD)(ScreenInfo.srWindow.Bottom - ScreenInfo.srWindow.Top + 1)) {
            Opts->ConsoleHeight = (DWORD)(ScreenInfo.srWindow.Bottom - ScreenInfo.srWindow.Top + 1);
        }

        //
        //  This is another kludge.  To make progress we need to be able
        //  to display one line of output between "press any key" prompts,
        //  so we need to assume the console can hold two lines.  If we
        //  don't make this assumption, we'll never go forward.
        //

        if (Opts->ConsoleHeight < 2) {
            Opts->ConsoleHeight = 2;
        }

        YoriLibSetColorToWin32(&Opts->PreviousAttributes, (UCHAR)(ScreenInfo.wAttributes & YORILIB_ATTRIBUTE_FULLCOLOR_MASK));
    }

    YoriLibResolveWindowColorComponents(SdirDefaultColor, Opts->PreviousAttributes, TRUE, &SdirDefaultColor);

    if (Opts->ConsoleWidth > SDIR_MAX_WIDTH) {
        Opts->ConsoleWidth = SDIR_MAX_WIDTH;
    }

    //
    //  Attempt to enable backup privilege.  This allows us to enumerate and recurse
    //  through objects which normally ACLs would prevent.
    //

    YoriLibEnableBackupPrivilege();

    //
    //  When running on WOW64, we don't want file system redirection,
    //  because we want people to be able to enumerate those paths
    //

    if (DllKernel32.pWow64DisableWow64FsRedirection) {
        PVOID DontCare;
        DllKernel32.pWow64DisableWow64FsRedirection(&DontCare);
    }

    //
    //  Grab the version of the running OS so we can highlight binaries that
    //  need a newer one
    //

    YoriLibGetOsVersion(&MajorVersion, &MinorVersion, &BuildNumber);
    Opts->OsVersion = ((BuildNumber & 0xFFFF) << 16) | ((MinorVersion & 0xFF) << 8) | (MajorVersion & 0xFF);

    //
    //  Look for Ctrl+C to indicate execution should terminate.
    //  Not much we can do on failure.
    //

    SetConsoleCtrlHandler(SdirCancelHandler, TRUE);

    return TRUE;
}

/**
 After command line options have been processed, initialize in memory state
 to ensure we can fulfil the user's requests.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SdirOptInitialize(VOID)
{
    ULONG i, j;
    YORILIB_COLOR_ATTRIBUTES Attr;
    memset(&Attr, 0, sizeof(Attr));

    //
    //  Calculate the amount of metadata in each column
    //

    Opts->MetadataWidth = 1;  // Column seperator

    //
    //  If no sorting algorithm was used, use the default that
    //  we prepopulated.
    //

    if (Opts->CurrentSort == 0) {
        Opts->CurrentSort++;
    }

    for (i = 0; i < SdirGetNumSdirOptions(); i++) {

        PSDIR_FEATURE Feature;

        Feature = SdirFeatureByOptionNumber(i);

        //
        //  If we're displaying or sorting, we need the data to use
        //

        if (Feature->Flags & SDIR_FEATURE_DISPLAY) {

            Feature->Flags |= SDIR_FEATURE_COLLECT;
        }

        for (j = 0; j < Opts->CurrentSort; j++) {

            if (SdirOptions[i].CompareFn == Opts->Sort[j].CompareFn) {
                Feature->Flags |= SDIR_FEATURE_COLLECT;
            }
        }

        //
        //  If we're displaying, we need space to display in
        //

        if ((Feature->Flags & SDIR_FEATURE_DISPLAY) &&
               SdirOptions[i].WidthFn) {

            Opts->MetadataWidth += SdirOptions[i].WidthFn(NULL, Attr, NULL);
        }
    }

    return TRUE;
}

/**
 Process a single command line option and configure in memory state to
 correspond to it.

 @param Opt Pointer to a NULL terminated string from the user specifying the
        option.

 @return TRUE if the option was parsed successfully, FALSE if it was not
         understood or not processed successfully.
 */
BOOL
SdirParseOpt (
    __in LPTSTR Opt
    )
{
    BOOL OptParsed = FALSE;
    DWORD i, j;

    if (Opt[0] == 'b') {
        if (Opt[1] == 'r' ||
            Opt[1] == 's') {

            SdirWriteString(_T("Brief recurse has been removed.  Use du instead.\n"));
        } else if (Opt[1] == '\0') {
            Opts->BasicEnumeration = TRUE;
            OptParsed = TRUE;
        }
    } else if (Opt[0] == 'c') {
        if (Opt[1] == 'w') {
            Opts->ConsoleWidth = SdirStringToNum32(&Opt[2], NULL);
            if (Opts->ConsoleWidth > SDIR_MAX_WIDTH) {
                Opts->ConsoleWidth = SDIR_MAX_WIDTH;
            }
            if (!Opts->OutputHasAutoLineWrap) {
                Opts->ConsoleBufferWidth = Opts->ConsoleWidth;
            }
            OptParsed = TRUE;
        }
    } else if (Opt[0] == 'd') {
        for (i = 0; i < SdirGetNumSdirOptions(); i++) {
            if (_tcsicmp(&Opt[1], SdirOptions[i].Switch) == 0 &&
                SdirOptions[i].Default.Flags & SDIR_FEATURE_ALLOW_DISPLAY) {

                PSDIR_FEATURE Feature;

                Feature = SdirFeatureByOptionNumber(i);
                Feature->Flags |= SDIR_FEATURE_DISPLAY;
                OptParsed = TRUE;
                break;
            }
        }
    } else if (Opt[0] == 'h') {
        for (i = 0; i < SdirGetNumSdirOptions(); i++) {
            if (_tcsicmp(&Opt[1], SdirOptions[i].Switch) == 0 &&
                SdirOptions[i].Default.Flags & SDIR_FEATURE_ALLOW_DISPLAY) {

                PSDIR_FEATURE Feature;

                Feature = SdirFeatureByOptionNumber(i);
                Feature->Flags &= ~(SDIR_FEATURE_DISPLAY);
                OptParsed = TRUE;
                break;
            }
        }
    } else if (Opt[0] == 'f') {
        if (Opt[1] == 'c') {
            DWORD Len = (DWORD)_tcslen(&Opt[2]);
            if (Len > 0) {
                if (!YoriLibAllocateString(&Opts->CustomFileColor, Len + 1)) {
                    return FALSE;
                }
                Opts->CustomFileColor.LengthInChars = YoriLibSPrintfS(Opts->CustomFileColor.StartOfString, Len + 1, _T("%s"), &Opt[2]);
                OptParsed = TRUE;
            }
        } else if (Opt[1] == 'e') {
            DWORD Len = (DWORD)_tcslen(&Opt[2]);
            if (Len > 0) {
                if (!YoriLibAllocateString(&Opts->CustomFileFilter, Len + 1)) {
                    return FALSE;
                }
                Opts->CustomFileFilter.LengthInChars = YoriLibSPrintfS(Opts->CustomFileFilter.StartOfString, Len + 1, _T("%s"), &Opt[2]);
                OptParsed = TRUE;
            }
        }
    } else if (Opt[0] == 'l') {
        if (Opt[1] == 'n') {
            Opts->TraverseLinks = FALSE;
            OptParsed = TRUE;
        } else if (Opt[1] == '\0') {
            Opts->TraverseLinks = TRUE;
            OptParsed = TRUE;
        }
    } else if (Opt[0] == 's' || Opt[0] == 'i') {

        //
        //  If we don't have space for it, don't even try
        //

        if (Opts->CurrentSort < sizeof(Opts->Sort)/sizeof(Opts->Sort[0])) {

            //
            //  Find which option it is
            //

            for (i = 0; i < SdirGetNumSdirOptions(); i++) {
                if (_tcsicmp(&Opt[1], SdirOptions[i].Switch) == 0 &&
                    SdirOptions[i].Default.Flags & SDIR_FEATURE_ALLOW_SORT) {

                    //
                    //  See if it's been specified before and silently discard
                    //  it.
                    //

                    for (j = 0; j < Opts->CurrentSort; j++) {
                        if (Opts->Sort[j].CompareFn == SdirOptions[i].CompareFn) {
                            OptParsed = TRUE;
                            break;
                        }
                    }

                    if (OptParsed) {
                        break;
                    }

                    Opts->Sort[Opts->CurrentSort].CompareFn = SdirOptions[i].CompareFn;

                    if (Opt[0] == 's') {
                        Opts->Sort[Opts->CurrentSort].CompareBreakCondition = YORI_LIB_GREATER_THAN;
                        Opts->Sort[Opts->CurrentSort].CompareInverseCondition = YORI_LIB_LESS_THAN;
                    } else {
                        Opts->Sort[Opts->CurrentSort].CompareBreakCondition = YORI_LIB_LESS_THAN;
                        Opts->Sort[Opts->CurrentSort].CompareInverseCondition = YORI_LIB_GREATER_THAN;
                    }
                    Opts->CurrentSort++;
                    OptParsed = TRUE;

                    break;
                }
            }

        } else {
        }
    } else if (Opt[0] == 'p') {
        if (Opt[1] == 'n') {
            Opts->EnablePause = FALSE;
            OptParsed = TRUE;
        } else if (Opt[1] == '\0') {
            if (Opts->OutputWithConsoleApi) {
                Opts->EnablePause = TRUE;
            }
            OptParsed = TRUE;
        }
    } else if (Opt[0] == 'r') {
        OptParsed = TRUE;
        Opts->Recursive = TRUE;
    } else if (Opt[0] == 't') {
        if (Opt[1] == 'n') {
            Opts->EnableNameTruncation = FALSE;
            OptParsed = TRUE;
        } else if (Opt[1] == '\0') {
            Opts->EnableNameTruncation = TRUE;
            OptParsed = TRUE;
        }
    } else if (Opt[0] == 'u') {
#ifdef UNICODE
        if (Opt[1] == 'n') {
            Opts->OutputExtendedCharacters = FALSE;
            OptParsed = TRUE;
        } else if (Opt[1] == '\0') {
            Opts->OutputExtendedCharacters = TRUE;
            OptParsed = TRUE;
        }
#endif
    }

    return OptParsed;
}

/**
 Parse command line arguments and configure in memory state.

 @param ArgC Count of arguments.

 @param ArgV Array of string arguments.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SdirParseArgs (
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    ULONG  CurrentArg;
    BOOL   OptParsed;
    BOOL   DisplayUsage;

    TCHAR  EnvOpts[200];
    LPTSTR Opt;
    LPTSTR TokContext = NULL;
    DWORD  i;

    YORI_STRING Arg;

    //
    //  Default to name sorting.  If something else is specified
    //  we clobber this entry.
    //

    Opts->CurrentSort = 0;
    Opts->Sort[0].CompareFn = YoriLibCompareFileName;
    Opts->Sort[0].CompareBreakCondition = YORI_LIB_GREATER_THAN;
    Opts->Sort[0].CompareInverseCondition = YORI_LIB_LESS_THAN;

    Opts->EnableNameTruncation = TRUE;

    for (i = 0; i < SdirGetNumSdirOptions(); i++) {
        PSDIR_FEATURE Feature;

        Feature = SdirFeatureByOptionNumber(i);
        Feature->Flags = SdirOptions[i].Default.Flags;
        YoriLibResolveWindowColorComponents(SdirOptions[i].Default.HighlightColor, Opts->PreviousAttributes, TRUE, &Feature->HighlightColor);
    }

    if (GetEnvironmentVariable(_T("SDIR_OPTS"), EnvOpts, sizeof(EnvOpts)/sizeof(EnvOpts[0]))) {
        EnvOpts[sizeof(EnvOpts)/sizeof(EnvOpts[0]) - 1] = '\0';
        Opt = _tcstok_s(EnvOpts, _T(" "), &TokContext);

        while (Opt) {

            if (YoriLibIsCommandLineOptionChar(Opt[0])) {

                Opt++;

                OptParsed = SdirParseOpt(Opt);

                if (!OptParsed) {
                    SdirWriteString(_T("Unknown environment option: "));
                    SdirWriteString(Opt);
                    SdirWriteString(_T("\n"));
                    SdirUsage(ArgC, ArgV);
                    return FALSE;
                }
            }

            Opt = _tcstok_s(NULL, _T(" "), &TokContext);
        }
    }

    for (CurrentArg = 1; CurrentArg < ArgC; CurrentArg++) {

        if (YoriLibIsCommandLineOption(&ArgV[CurrentArg], &Arg)) {

            OptParsed = FALSE;
            DisplayUsage = FALSE;

            OptParsed = SdirParseOpt(Arg.StartOfString);

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("help")) == 0) {
                DisplayUsage = TRUE;
                OptParsed = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                DisplayUsage = TRUE;
                OptParsed = TRUE;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("v")) == 0) {
                DisplayUsage = TRUE;
                OptParsed = TRUE;
            }

            if (DisplayUsage || !OptParsed) {
                if (!OptParsed) {
                    SdirWriteString(_T("Unknown argument: "));
                    SdirWriteString(ArgV[CurrentArg].StartOfString);
                    SdirWriteString(_T("\n"));
                }
                SdirUsage(ArgC, ArgV);
                return FALSE;
            }
        }
    }

    return TRUE;
}

/**
 Initialize the application, parsing all arguments and configuring global
 state ready for execution.

 @param ArgC Count of arguments.

 @param ArgV Array of string arguments.

 @return TRUE to indicate success, FALSE to indicate failure.
 */
BOOL
SdirInit(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    if (!YoriLibLoadKernel32Functions()) {
        return FALSE;
    }

    if (!SdirAppInitialize()) {
        return FALSE;
    }

    if (!SdirParseArgs(ArgC, ArgV)) {
        return FALSE;
    }

    if (!SdirOptInitialize()) {
        return FALSE;
    }

    if (!SdirParseAttributeApplyString()) {
        return FALSE;
    }

    if (!SdirParseMetadataAttributeString()) {
        return FALSE;
    }

    return TRUE;
}


/**
 Tear down any global allocations caused by invoking the application.
 */
VOID
SdirAppCleanup(VOID)
{
    SetConsoleCtrlHandler(SdirCancelHandler, FALSE);
    if (Opts != NULL) {
        YoriLibFreeStringContents(&Opts->CustomFileFilter);
        YoriLibFreeStringContents(&Opts->CustomFileColor);
        YoriLibFreeStringContents(&Opts->ParentName);
        YoriLibFree(Opts);
        Opts = NULL;
    }

    if (Summary != NULL) {
        YoriLibFree(Summary);
        Summary = NULL;
    }

    YoriLibFileFiltFreeFilter(&SdirGlobal.FileColorCriteria);
    YoriLibFileFiltFreeFilter(&SdirGlobal.FileHideCriteria);

    if (SdirDirCollection != NULL) {
        YoriLibFree(SdirDirCollection);
        SdirDirCollection = NULL;
    }

    if (SdirDirSorted != NULL) {
        YoriLibFree(SdirDirSorted);
        SdirDirSorted = NULL;
    }
}

// vim:sw=4:ts=4:et:


<|start_filename|>cls/cls.c<|end_filename|>
/**
 * @file cls/cls.c
 *
 * Yori shell display command line output
 *
 * Copyright (c) 2017-2018 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>

/**
 Help text to display to the user.
 */
const
CHAR strClsHelpText[] =
        "\n"
        "Clears the console.\n"
        "\n"
        "CLS [-license]\n";

/**
 Display usage text to the user.
 */
BOOL
ClsHelp(VOID)
{
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("Cls %i.%02i\n"), YORI_VER_MAJOR, YORI_VER_MINOR);
#if YORI_BUILD_ID
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("  Build %i\n"), YORI_BUILD_ID);
#endif
    YoriLibOutput(YORI_LIB_OUTPUT_STDOUT, _T("%hs"), strClsHelpText);
    return TRUE;
}

#ifdef YORI_BUILTIN
/**
 The main entrypoint for the cls builtin command.
 */
#define ENTRYPOINT YoriCmd_YCLS
#else
/**
 The main entrypoint for the cls standalone application.
 */
#define ENTRYPOINT ymain
#endif

/**
 The main entrypoint for the cls cmdlet.

 @param ArgC The number of arguments.

 @param ArgV An array of arguments.

 @return Exit code of the child process on success, or failure if the child
         could not be launched.
 */
DWORD
ENTRYPOINT(
    __in DWORD ArgC,
    __in YORI_STRING ArgV[]
    )
{
    CONSOLE_SCREEN_BUFFER_INFO BufferInfo;
    DWORD CharsWritten;
    BOOL ArgumentUnderstood;
    DWORD i;
    YORI_STRING Arg;
    HANDLE hConOut;

    for (i = 1; i < ArgC; i++) {

        ASSERT(YoriLibIsStringNullTerminated(&ArgV[i]));

        ArgumentUnderstood = FALSE;

        if (YoriLibIsCommandLineOption(&ArgV[i], &Arg)) {

            if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("?")) == 0) {
                ClsHelp();
                return EXIT_SUCCESS;
            } else if (YoriLibCompareStringWithLiteralInsensitive(&Arg, _T("license")) == 0) {
                YoriLibDisplayMitLicense(_T("2017-2018"));
                return EXIT_SUCCESS;
            }
        } else {
            ArgumentUnderstood = TRUE;
            break;
        }

        if (!ArgumentUnderstood) {
            YoriLibOutput(YORI_LIB_OUTPUT_STDERR, _T("Argument not understood, ignored: %y\n"), ArgV[i]);
        }
    }

    hConOut = GetStdHandle(STD_OUTPUT_HANDLE);

    //
    //  We can't clear the screen of non-console devices
    //

    if (!GetConsoleScreenBufferInfo(hConOut, &BufferInfo)) {
        return EXIT_FAILURE;
    }

    //
    //  If the device supports VT sequences, try to clear the scrollback
    //  buffer
    //

    if (SetConsoleMode(hConOut, ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING)) {
        YoriLibOutput(YORI_LIB_OUTPUT_STDOUT | YORI_LIB_OUTPUT_PASSTHROUGH_VT, _T("\x1b[2J\x1b[3J"));
    }

    //
    //  Now clear all addressable cells
    //

    BufferInfo.dwCursorPosition.X = 0;
    BufferInfo.dwCursorPosition.Y = 0;

    FillConsoleOutputCharacter(hConOut, ' ', (DWORD)BufferInfo.dwSize.X * BufferInfo.dwSize.Y, BufferInfo.dwCursorPosition, &CharsWritten);

    FillConsoleOutputAttribute(hConOut, BufferInfo.wAttributes, (DWORD)BufferInfo.dwSize.X * BufferInfo.dwSize.Y, BufferInfo.dwCursorPosition, &CharsWritten);

    SetConsoleCursorPosition(hConOut, BufferInfo.dwCursorPosition);


    return EXIT_SUCCESS;
}

// vim:sw=4:ts=4:et:


<|start_filename|>date/Makefile<|end_filename|>

BINARIES=ydate.exe

!INCLUDE "..\config\common.mk"

!IF $(PDB)==1
LINKPDB=/Pdb:ydate.pdb
!ENDIF

BIN_OBJS=\
	 date.obj        \

MOD_OBJS=\
	 mod_date.obj    \

compile: $(BIN_OBJS) builtins.lib

ydate.exe: $(BIN_OBJS) $(YORILIB) $(YORICRT) $(YORIVER)
	@echo $@
	@$(LINK) $(LDFLAGS) -entry:$(YENTRY) $(BIN_OBJS) $(LIBS) $(CRTLIB) $(YORILIB) $(YORIVER) -version:$(YORI_VER_MAJOR).$(YORI_VER_MINOR) $(LINKPDB) -out:$@

mod_date.obj: date.c
	@echo $@
	@$(CC) -c -DYORI_BUILTIN=1 $(CFLAGS) -Fo$@ date.c

builtins.lib: $(MOD_OBJS)
	@echo $@
	@$(LIB32) $(LIBFLAGS) $(MOD_OBJS) -out:$@



<|start_filename|>libdlg/msgbox.c<|end_filename|>
/**
 * @file libdlg/msgbox.c
 *
 * Yori shell message dialog
 *
 * Copyright (c) 2019-2020 <NAME>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <yoripch.h>
#include <yorilib.h>
#include <yoriwin.h>

/**
 A callback invoked when a button is clicked.

 @param Ctrl Pointer to the button that was clicked.
 */
VOID
YoriDlgMsgButtonClicked(
    __in PYORI_WIN_CTRL_HANDLE Ctrl
    )
{
    PYORI_WIN_CTRL_HANDLE Parent;
    DWORD_PTR CtrlId;
    Parent = YoriWinGetControlParent(Ctrl);
    CtrlId = YoriWinGetControlId(Ctrl);
    YoriWinCloseWindow(Parent, CtrlId);
}

/**
 Display a dialog box to display a message and allow the user to click
 a button

 @param WinMgrHandle Pointer to the window manager.

 @param Title The string to display in the title of the dialog.

 @param Text The string to display in the body of the dialog.

 @param NumButtons The number of buttons to display.

 @param ButtonTexts An array of NumButtons length of Yori strings to display
        in the buttons.

 @param DefaultIndex The zero-based index of the button which should be
        invoked when the user presses enter.

 @param CancelIndex The zero-based index of the button which should be
        invoked when the user presses escape.

 @return Zero to indicate failure.  A nonzero value indicates the element of
         the ButtonTexts array that was clicked, where the first button has
         a value of one.
 */
__success(return)
DWORD
YoriDlgMessageBox(
    __in PYORI_WIN_WINDOW_MANAGER_HANDLE WinMgrHandle,
    __in PYORI_STRING Title,
    __in PYORI_STRING Text,
    __in DWORD NumButtons,
    __in PYORI_STRING ButtonTexts,
    __in DWORD DefaultIndex,
    __in DWORD CancelIndex
    )
{
    PYORI_WIN_WINDOW_HANDLE Parent;
    COORD WindowSize;
    SMALL_RECT TextArea;
    SMALL_RECT ButtonArea;
    DWORD Index;
    DWORD DisplayLength;
    DWORD TotalButtonWidth;
    DWORD LabelLinesRequired;
    DWORD LabelWidthRequired;
    DWORD Style;
    WORD WindowWidth;
    WORD WindowHeight;
    PYORI_WIN_CTRL_HANDLE Ctrl;
    DWORD_PTR Result;

    if (!YoriWinGetWinMgrDimensions(WinMgrHandle, &WindowSize)) {
        return FALSE;
    }

    //
    //  The window decoration will take six characters (two border, two
    //  shadow, two padding between label text and window.)  We take a
    //  few extra characters off just for visual reasons.
    //

    DisplayLength = WindowSize.X - 10;
    LabelLinesRequired = YoriWinLabelCountLinesRequiredForText(Text, DisplayLength, &LabelWidthRequired);

    //
    //  Vertically, the window has 8 lines of overhead (title bar, padding
    //  above text, padding below text, three lines of buttons, border, and
    //  shadow.)
    //

    if (LabelLinesRequired + 8 > (WORD)WindowSize.Y &&
        WindowSize.Y > 8) {

        LabelLinesRequired = WindowSize.Y - 8;
    }

    WindowWidth = (WORD)(LabelWidthRequired + 6);
    WindowHeight = (WORD)(LabelLinesRequired + 8);

    if (!YoriWinCreateWindow(WinMgrHandle, WindowWidth, WindowHeight, WindowWidth, WindowHeight, YORI_WIN_WINDOW_STYLE_BORDER_SINGLE | YORI_WIN_WINDOW_STYLE_SHADOW_SOLID, Title, &Parent)) {
        return FALSE;
    }

    YoriWinGetClientSize(Parent, &WindowSize);

    TextArea.Left = 1;
    TextArea.Top = 1;
    TextArea.Right = (WORD)(WindowSize.X - 2);
    TextArea.Bottom = (SHORT)(TextArea.Top + LabelLinesRequired - 1);

    Ctrl = YoriWinLabelCreate(Parent, &TextArea, Text, YORI_WIN_LABEL_STYLE_CENTER | YORI_WIN_LABEL_NO_ACCELERATOR);
    if (Ctrl == NULL) {
        YoriWinDestroyWindow(Parent);
        return FALSE;
    }

    //
    //  Count the number of characters in the buttons.
    //

    TotalButtonWidth = 0;
    for (Index = 0; Index < NumButtons; Index++) {

        YoriWinLabelParseAccelerator(&ButtonTexts[Index],
                                     NULL,
                                     NULL,
                                     NULL,
                                     &DisplayLength);

        TotalButtonWidth += DisplayLength;
    }

    //
    //  Each button also has a space before and after the text, a border
    //  left and right, and one char between the buttons.
    //

    TotalButtonWidth += 5 * NumButtons - 1;

    ButtonArea.Top = (SHORT)(TextArea.Bottom + 2);
    ButtonArea.Bottom = (SHORT)(ButtonArea.Top + 2);
    ButtonArea.Left = (SHORT)((WindowSize.X - TotalButtonWidth) / 2);

    for (Index = 0; Index < NumButtons; Index++) {

        YoriWinLabelParseAccelerator(&ButtonTexts[Index],
                                     NULL,
                                     NULL,
                                     NULL,
                                     &DisplayLength);

        ButtonArea.Right = (SHORT)(ButtonArea.Left + 3 + DisplayLength);

        Style = 0;

        if (Index == DefaultIndex) {
            Style |= YORI_WIN_BUTTON_STYLE_DEFAULT;
        }

        if (Index == CancelIndex) {
            Style |= YORI_WIN_BUTTON_STYLE_CANCEL;
        }

        Ctrl = YoriWinButtonCreate(Parent, &ButtonArea, &ButtonTexts[Index], Style, YoriDlgMsgButtonClicked);
        if (Ctrl == NULL) {
            YoriWinDestroyWindow(Parent);
            return FALSE;
        }

        YoriWinSetControlId(Ctrl, Index + 1);
        ButtonArea.Left = (SHORT)(ButtonArea.Right + 2);
    }

    Result = 0;
    YoriWinEnableNonAltAccelerators(Parent, TRUE);
    if (!YoriWinProcessInputForWindow(Parent, &Result)) {
        Result = 0;
    }
    YoriWinDestroyWindow(Parent);
    return (DWORD)Result;
}

// vim:sw=4:ts=4:et:
