Git conversion: Make reactos the root directory, move rosapps, rostests, wallpapers...
[reactos.git] / win32ss / printing / providers / localspl / jobs.c
diff --git a/win32ss/printing/providers/localspl/jobs.c b/win32ss/printing/providers/localspl/jobs.c
new file mode 100644 (file)
index 0000000..0c9e040
--- /dev/null
@@ -0,0 +1,1509 @@
+/*
+ * PROJECT:     ReactOS Local Spooler
+ * LICENSE:     GPL-2.0+ (https://spdx.org/licenses/GPL-2.0+)
+ * PURPOSE:     Functions for managing print jobs
+ * COPYRIGHT:   Copyright 2015-2017 Colin Finck (colin@reactos.org)
+ */
+
+#include "precomp.h"
+
+// Global Variables
+SKIPLIST GlobalJobList;
+
+// Local Variables
+static DWORD _dwLastJobID;
+
+// Local Constants
+static DWORD dwJobInfo1Offsets[] = {
+    FIELD_OFFSET(JOB_INFO_1W, pPrinterName),
+    FIELD_OFFSET(JOB_INFO_1W, pMachineName),
+    FIELD_OFFSET(JOB_INFO_1W, pUserName),
+    FIELD_OFFSET(JOB_INFO_1W, pDocument),
+    FIELD_OFFSET(JOB_INFO_1W, pDatatype),
+    FIELD_OFFSET(JOB_INFO_1W, pStatus),
+    MAXDWORD
+};
+
+static DWORD dwJobInfo2Offsets[] = {
+    FIELD_OFFSET(JOB_INFO_2W, pPrinterName),
+    FIELD_OFFSET(JOB_INFO_2W, pMachineName),
+    FIELD_OFFSET(JOB_INFO_2W, pUserName),
+    FIELD_OFFSET(JOB_INFO_2W, pDocument),
+    FIELD_OFFSET(JOB_INFO_2W, pNotifyName),
+    FIELD_OFFSET(JOB_INFO_2W, pDatatype),
+    FIELD_OFFSET(JOB_INFO_2W, pPrintProcessor),
+    FIELD_OFFSET(JOB_INFO_2W, pParameters),
+    FIELD_OFFSET(JOB_INFO_2W, pDriverName),
+    FIELD_OFFSET(JOB_INFO_2W, pStatus),
+    MAXDWORD
+};
+
+
+/**
+ * @name _EqualStrings
+ *
+ * Returns whether two strings are equal.
+ * Unlike wcscmp, this function also works with NULL strings.
+ *
+ * @param pwszA
+ * First string to compare.
+ *
+ * @param pwszB
+ * Second string to compare.
+ *
+ * @return
+ * TRUE if the strings are equal, FALSE if they differ.
+ */
+static __inline BOOL
+_EqualStrings(PCWSTR pwszA, PCWSTR pwszB)
+{
+    if (!pwszA && !pwszB)
+        return TRUE;
+
+    if (pwszA && !pwszB)
+        return FALSE;
+
+    if (!pwszA && pwszB)
+        return FALSE;
+
+    return (wcscmp(pwszA, pwszB) == 0);
+}
+
+static BOOL
+_GetNextJobID(PDWORD dwJobID)
+{
+    ++_dwLastJobID;
+
+    while (LookupElementSkiplist(&GlobalJobList, &_dwLastJobID, NULL))
+    {
+        // This ID is already taken. Try the next one.
+        ++_dwLastJobID;
+    }
+
+    if (!IS_VALID_JOB_ID(_dwLastJobID))
+    {
+        ERR("Job ID %lu isn't valid!\n", _dwLastJobID);
+        return FALSE;
+    }
+
+    *dwJobID = _dwLastJobID;
+    return TRUE;
+}
+
+/**
+ * @name _GlobalJobListCompareRoutine
+ *
+ * SKIPLIST_COMPARE_ROUTINE for the Global Job List.
+ * We need the Global Job List to check whether a Job ID is already in use. Consequently, this list is sorted by ID.
+ */
+static int WINAPI
+_GlobalJobListCompareRoutine(PVOID FirstStruct, PVOID SecondStruct)
+{
+    PLOCAL_JOB A = (PLOCAL_JOB)FirstStruct;
+    PLOCAL_JOB B = (PLOCAL_JOB)SecondStruct;
+
+    return A->dwJobID - B->dwJobID;
+}
+
+/**
+ * @name _PrinterJobListCompareRoutine
+ *
+ * SKIPLIST_COMPARE_ROUTINE for the each Printer's Job List.
+ * Jobs in this list are sorted in the desired order of processing.
+ */
+static int WINAPI
+_PrinterJobListCompareRoutine(PVOID FirstStruct, PVOID SecondStruct)
+{
+    PLOCAL_JOB A = (PLOCAL_JOB)FirstStruct;
+    PLOCAL_JOB B = (PLOCAL_JOB)SecondStruct;
+    int iComparison;
+    FILETIME ftSubmittedA;
+    FILETIME ftSubmittedB;
+    ULARGE_INTEGER uliSubmittedA;
+    ULARGE_INTEGER uliSubmittedB;
+    ULONGLONG ullResult;
+
+    // First compare the priorities to determine the order.
+    // The job with a higher priority shall come first.
+    iComparison = A->dwPriority - B->dwPriority;
+    if (iComparison != 0)
+        return iComparison;
+
+    // Both have the same priority, so go by creation time.
+    // Comparison is done using the MSDN-recommended way for comparing SYSTEMTIMEs.
+    if (!SystemTimeToFileTime(&A->stSubmitted, &ftSubmittedA))
+    {
+        ERR("SystemTimeToFileTime failed for A with error %lu!\n", GetLastError());
+        return 0;
+    }
+
+    if (!SystemTimeToFileTime(&B->stSubmitted, &ftSubmittedB))
+    {
+        ERR("SystemTimeToFileTime failed for B with error %lu!\n", GetLastError());
+        return 0;
+    }
+
+    uliSubmittedA.LowPart = ftSubmittedA.dwLowDateTime;
+    uliSubmittedA.HighPart = ftSubmittedA.dwHighDateTime;
+    uliSubmittedB.LowPart = ftSubmittedB.dwLowDateTime;
+    uliSubmittedB.HighPart = ftSubmittedB.dwHighDateTime;
+    ullResult = uliSubmittedA.QuadPart - uliSubmittedB.QuadPart;
+
+    if (ullResult < 0)
+        return -1;
+    else if (ullResult > 0)
+        return 1;
+
+    return 0;
+}
+
+DWORD
+GetJobFilePath(PCWSTR pwszExtension, DWORD dwJobID, PWSTR pwszOutput)
+{
+    TRACE("GetJobFilePath(%S, %lu, %p)\n", pwszExtension, dwJobID, pwszOutput);
+
+    if (pwszOutput)
+    {
+        CopyMemory(pwszOutput, wszJobDirectory, cchJobDirectory * sizeof(WCHAR));
+        swprintf(&pwszOutput[cchJobDirectory], L"\\%05lu.%s", dwJobID, pwszExtension);
+    }
+
+    // pwszExtension may be L"SPL" or L"SHD", same length for both!
+    return (cchJobDirectory + sizeof("\\?????.SPL")) * sizeof(WCHAR);
+}
+
+BOOL
+InitializeGlobalJobList(void)
+{
+    const WCHAR wszPath[] = L"\\?????.SHD";
+    const DWORD cchPath = _countof(wszPath) - 1;
+
+    DWORD dwErrorCode;
+    DWORD dwJobID;
+    HANDLE hFind;
+    PLOCAL_JOB pJob = NULL;
+    PWSTR p;
+    WCHAR wszFullPath[MAX_PATH];
+    WIN32_FIND_DATAW FindData;
+
+    TRACE("InitializeGlobalJobList()\n");
+
+    // This one is incremented in _GetNextJobID.
+    _dwLastJobID = 0;
+
+    // Initialize an empty list for all jobs of all local printers.
+    // We will search it by Job ID (supply a pointer to a DWORD in LookupElementSkiplist).
+    InitializeSkiplist(&GlobalJobList, DllAllocSplMem, _GlobalJobListCompareRoutine, (PSKIPLIST_FREE_ROUTINE)DllFreeSplMem);
+
+    // Construct the full path search pattern.
+    CopyMemory(wszFullPath, wszJobDirectory, cchJobDirectory * sizeof(WCHAR));
+    CopyMemory(&wszFullPath[cchJobDirectory], wszPath, (cchPath + 1) * sizeof(WCHAR));
+
+    // Use the search pattern to look for unfinished jobs serialized in shadow files (.SHD)
+    hFind = FindFirstFileW(wszFullPath, &FindData);
+    if (hFind == INVALID_HANDLE_VALUE)
+    {
+        // No unfinished jobs found.
+        dwErrorCode = ERROR_SUCCESS;
+        goto Cleanup;
+    }
+
+    do
+    {
+        // Skip possible subdirectories.
+        if (FindData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
+            continue;
+
+        // Extract the Job ID and verify the file name format at the same time.
+        // This includes all valid names (like "00005.SHD") and excludes invalid ones (like "10ABC.SHD").
+        dwJobID = wcstoul(FindData.cFileName, &p, 10);
+        if (!IS_VALID_JOB_ID(dwJobID))
+            continue;
+
+        if (wcsicmp(p, L".SHD") != 0)
+            continue;
+
+        // This shadow file has a valid name. Construct the full path and try to load it.
+        GetJobFilePath(L"SHD", dwJobID, wszFullPath);
+        pJob = ReadJobShadowFile(wszFullPath);
+        if (!pJob)
+            continue;
+
+        // Add it to the Global Job List.
+        if (!InsertElementSkiplist(&GlobalJobList, pJob))
+        {
+            dwErrorCode = ERROR_NOT_ENOUGH_MEMORY;
+            ERR("InsertElementSkiplist failed for job %lu for the GlobalJobList!\n", pJob->dwJobID);
+            goto Cleanup;
+        }
+
+        // Add it to the Printer's Job List.
+        if (!InsertElementSkiplist(&pJob->pPrinter->JobList, pJob))
+        {
+            dwErrorCode = ERROR_NOT_ENOUGH_MEMORY;
+            ERR("InsertElementSkiplist failed for job %lu for the Printer's Job List!\n", pJob->dwJobID);
+            goto Cleanup;
+        }
+    }
+    while (FindNextFileW(hFind, &FindData));
+
+    dwErrorCode = ERROR_SUCCESS;
+
+Cleanup:
+    // Outside the loop
+    if (hFind)
+        FindClose(hFind);
+
+    SetLastError(dwErrorCode);
+    return (dwErrorCode == ERROR_SUCCESS);
+}
+
+void
+InitializePrinterJobList(PLOCAL_PRINTER pPrinter)
+{
+    TRACE("InitializePrinterJobList(%p)\n", pPrinter);
+
+    // Initialize an empty list for this printer's jobs.
+    // This one is only for sorting the jobs. If you need to lookup a job, search the GlobalJobList by Job ID.
+    InitializeSkiplist(&pPrinter->JobList, DllAllocSplMem, _PrinterJobListCompareRoutine, (PSKIPLIST_FREE_ROUTINE)DllFreeSplMem);
+}
+
+DWORD WINAPI
+CreateJob(PLOCAL_PRINTER_HANDLE pPrinterHandle)
+{
+    const WCHAR wszDoubleBackslash[] = L"\\";
+    const DWORD cchDoubleBackslash = _countof(wszDoubleBackslash) - 1;
+
+    DWORD cchMachineName;
+    DWORD cchUserName;
+    DWORD dwErrorCode;
+    PLOCAL_JOB pJob;
+    RPC_BINDING_HANDLE hServerBinding = NULL;
+    RPC_WSTR pwszBinding = NULL;
+    RPC_WSTR pwszMachineName = NULL;
+
+    TRACE("CreateJob(%p)\n", pPrinterHandle);
+
+    // Create a new job.
+    pJob = DllAllocSplMem(sizeof(LOCAL_JOB));
+    if (!pJob)
+    {
+        dwErrorCode = ERROR_NOT_ENOUGH_MEMORY;
+        ERR("DllAllocSplMem failed!\n");
+        goto Cleanup;
+    }
+
+    // Reserve an ID for this job.
+    if (!_GetNextJobID(&pJob->dwJobID))
+    {
+        dwErrorCode = ERROR_NOT_ENOUGH_MEMORY;
+        goto Cleanup;
+    }
+
+    // Copy over defaults to the LOCAL_JOB structure.
+    pJob->pPrinter = pPrinterHandle->pPrinter;
+    pJob->pPrintProcessor = pPrinterHandle->pPrinter->pPrintProcessor;
+    pJob->dwPriority = DEF_PRIORITY;
+    pJob->dwStatus = JOB_STATUS_SPOOLING;
+    pJob->pwszDatatype = AllocSplStr(pPrinterHandle->pwszDatatype);
+    pJob->pwszDocumentName = AllocSplStr(wszDefaultDocumentName);
+    pJob->pDevMode = DuplicateDevMode(pPrinterHandle->pDevMode);
+    GetSystemTime(&pJob->stSubmitted);
+
+    // Get the user name for the Job.
+    cchUserName = UNLEN + 1;
+    pJob->pwszUserName = DllAllocSplMem(cchUserName * sizeof(WCHAR));
+    if (!GetUserNameW(pJob->pwszUserName, &cchUserName))
+    {
+        dwErrorCode = GetLastError();
+        ERR("GetUserNameW failed with error %lu!\n", dwErrorCode);
+        goto Cleanup;
+    }
+
+    // FIXME: For now, pwszNotifyName equals pwszUserName.
+    pJob->pwszNotifyName = AllocSplStr(pJob->pwszUserName);
+
+    // Get the name of the machine that submitted the Job over RPC.
+    dwErrorCode = RpcBindingServerFromClient(NULL, &hServerBinding);
+    if (dwErrorCode != RPC_S_OK)
+    {
+        ERR("RpcBindingServerFromClient failed with status %lu!\n", dwErrorCode);
+        goto Cleanup;
+    }
+
+    dwErrorCode = RpcBindingToStringBindingW(hServerBinding, &pwszBinding);
+    if (dwErrorCode != RPC_S_OK)
+    {
+        ERR("RpcBindingToStringBindingW failed with status %lu!\n", dwErrorCode);
+        goto Cleanup;
+    }
+
+    dwErrorCode = RpcStringBindingParseW(pwszBinding, NULL, NULL, &pwszMachineName, NULL, NULL);
+    if (dwErrorCode != RPC_S_OK)
+    {
+        ERR("RpcStringBindingParseW failed with status %lu!\n", dwErrorCode);
+        goto Cleanup;
+    }
+
+    cchMachineName = wcslen(pwszMachineName);
+    pJob->pwszMachineName = DllAllocSplMem((cchMachineName + cchDoubleBackslash + 1) * sizeof(WCHAR));
+    CopyMemory(pJob->pwszMachineName, wszDoubleBackslash, cchDoubleBackslash * sizeof(WCHAR));
+    CopyMemory(&pJob->pwszMachineName[cchDoubleBackslash], pwszMachineName, (cchMachineName + 1) * sizeof(WCHAR));
+
+    // Add the job to the Global Job List.
+    if (!InsertElementSkiplist(&GlobalJobList, pJob))
+    {
+        dwErrorCode = ERROR_NOT_ENOUGH_MEMORY;
+        ERR("InsertElementSkiplist failed for job %lu for the GlobalJobList!\n", pJob->dwJobID);
+        goto Cleanup;
+    }
+
+    // Add the job at the end of the Printer's Job List.
+    // As all new jobs are created with default priority, we can be sure that it would always be inserted at the end.
+    if (!InsertTailElementSkiplist(&pJob->pPrinter->JobList, pJob))
+    {
+        dwErrorCode = ERROR_NOT_ENOUGH_MEMORY;
+        ERR("InsertTailElementSkiplist failed for job %lu for the Printer's Job List!\n", pJob->dwJobID);
+        goto Cleanup;
+    }
+
+    // We were successful!
+    pPrinterHandle->bStartedDoc = TRUE;
+    pPrinterHandle->pJob = pJob;
+    dwErrorCode = ERROR_SUCCESS;
+
+    // Don't let the cleanup routine free this.
+    pJob = NULL;
+
+Cleanup:
+    if (pJob)
+        DllFreeSplMem(pJob);
+
+    if (pwszMachineName)
+        RpcStringFreeW(&pwszMachineName);
+
+    if (pwszBinding)
+        RpcStringFreeW(&pwszBinding);
+
+    if (hServerBinding)
+        RpcBindingFree(&hServerBinding);
+
+    return dwErrorCode;
+}
+
+BOOL WINAPI
+LocalAddJob(HANDLE hPrinter, DWORD Level, PBYTE pData, DWORD cbBuf, PDWORD pcbNeeded)
+{
+    ADDJOB_INFO_1W AddJobInfo1;
+    DWORD dwErrorCode;
+    PLOCAL_HANDLE pHandle = (PLOCAL_HANDLE)hPrinter;
+    PLOCAL_PRINTER_HANDLE pPrinterHandle;
+
+    TRACE("LocalAddJob(%p, %lu, %p, %lu, %p)\n", hPrinter, Level, pData, cbBuf, pcbNeeded);
+
+    // Check if this is a printer handle.
+    if (pHandle->HandleType != HandleType_Printer)
+    {
+        dwErrorCode = ERROR_INVALID_HANDLE;
+        goto Cleanup;
+    }
+
+    pPrinterHandle = (PLOCAL_PRINTER_HANDLE)pHandle->pSpecificHandle;
+
+    // This handle must not have started a job yet!
+    if (pPrinterHandle->pJob)
+    {
+        dwErrorCode = ERROR_INVALID_HANDLE;
+        goto Cleanup;
+    }
+
+    // Check if this is the right structure level.
+    if (Level != 1)
+    {
+        dwErrorCode = ERROR_INVALID_LEVEL;
+        goto Cleanup;
+    }
+
+    // Check if the printer is set to do direct printing.
+    // The Job List isn't used in this case.
+    if (pPrinterHandle->pPrinter->dwAttributes & PRINTER_ATTRIBUTE_DIRECT)
+    {
+        dwErrorCode = ERROR_INVALID_ACCESS;
+        goto Cleanup;
+    }
+
+    // Check if the supplied buffer is large enough.
+    *pcbNeeded = sizeof(ADDJOB_INFO_1W) + GetJobFilePath(L"SPL", 0, NULL);
+    if (cbBuf < *pcbNeeded)
+    {
+        dwErrorCode = ERROR_INSUFFICIENT_BUFFER;
+        goto Cleanup;
+    }
+
+    // All requirements are met - create a new job.
+    dwErrorCode = CreateJob(pPrinterHandle);
+    if (dwErrorCode != ERROR_SUCCESS)
+        goto Cleanup;
+
+    // Mark that this job was started with AddJob (so that it can be scheduled for printing with ScheduleJob).
+    pPrinterHandle->pJob->bAddedJob = TRUE;
+
+    // Return a proper ADDJOB_INFO_1W structure.
+    AddJobInfo1.JobId = pPrinterHandle->pJob->dwJobID;
+    AddJobInfo1.Path = (PWSTR)(pData + sizeof(ADDJOB_INFO_1W));
+
+    CopyMemory(pData, &AddJobInfo1, sizeof(ADDJOB_INFO_1W));
+    GetJobFilePath(L"SPL", AddJobInfo1.JobId, AddJobInfo1.Path);
+
+Cleanup:
+    SetLastError(dwErrorCode);
+    return (dwErrorCode == ERROR_SUCCESS);
+}
+
+
+static void
+_LocalGetJobLevel1(PLOCAL_JOB pJob, PJOB_INFO_1W* ppJobInfo, PBYTE* ppJobInfoEnd, PDWORD pcbNeeded)
+{
+    DWORD cbDatatype;
+    DWORD cbDocumentName = 0;
+    DWORD cbMachineName;
+    DWORD cbPrinterName;
+    DWORD cbStatus = 0;
+    DWORD cbUserName = 0;
+    PWSTR pwszStrings[6];
+
+    // Calculate the string lengths.
+    if (!ppJobInfo)
+    {
+        cbDatatype = (wcslen(pJob->pwszDatatype) + 1) * sizeof(WCHAR);
+        cbMachineName = (wcslen(pJob->pwszMachineName) + 1) * sizeof(WCHAR);
+        cbPrinterName = (wcslen(pJob->pPrinter->pwszPrinterName) + 1) * sizeof(WCHAR);
+
+        // These values are optional.
+        if (pJob->pwszDocumentName)
+            cbDocumentName = (wcslen(pJob->pwszDocumentName) + 1) * sizeof(WCHAR);
+
+        if (pJob->pwszStatus)
+            cbStatus = (wcslen(pJob->pwszStatus) + 1) * sizeof(WCHAR);
+
+        if (pJob->pwszUserName)
+            cbUserName = (wcslen(pJob->pwszUserName) + 1) * sizeof(WCHAR);
+
+        *pcbNeeded += sizeof(JOB_INFO_1W) + cbDatatype + cbDocumentName + cbMachineName + cbPrinterName + cbStatus + cbUserName;
+        return;
+    }
+
+    // Set the general fields.
+    (*ppJobInfo)->JobId = pJob->dwJobID;
+    (*ppJobInfo)->Status = pJob->dwStatus;
+    (*ppJobInfo)->Priority = pJob->dwPriority;
+    (*ppJobInfo)->TotalPages = pJob->dwTotalPages;
+    (*ppJobInfo)->PagesPrinted = pJob->dwPagesPrinted;
+    CopyMemory(&(*ppJobInfo)->Submitted, &pJob->stSubmitted, sizeof(SYSTEMTIME));
+
+    // Position in JOB_INFO_1W is the 1-based index of the job in the processing queue.
+    // Retrieve this through the element index of the job in the Printer's Job List.
+    if (!LookupElementSkiplist(&pJob->pPrinter->JobList, pJob, &(*ppJobInfo)->Position))
+    {
+        ERR("pJob could not be located in the Printer's Job List!\n");
+        return;
+    }
+
+    // Make the index 1-based.
+    ++(*ppJobInfo)->Position;
+
+    // Set the pPrinterName field.
+    pwszStrings[0] = pJob->pPrinter->pwszPrinterName;
+
+    // Set the pMachineName field.
+    pwszStrings[1] = pJob->pwszMachineName;
+
+    // Set the pUserName field.
+    pwszStrings[2] = pJob->pwszUserName;
+
+    // Set the pDocument field.
+    pwszStrings[3] = pJob->pwszDocumentName;
+
+    // Set the pDatatype field.
+    pwszStrings[4] = pJob->pwszDatatype;
+
+    // Set the pStatus field.
+    pwszStrings[5] = pJob->pwszStatus;
+
+    // Finally copy the structure and advance to the next one in the output buffer.
+    *ppJobInfoEnd = PackStrings(pwszStrings, (PBYTE)(*ppJobInfo), dwJobInfo1Offsets, *ppJobInfoEnd);
+    (*ppJobInfo)++;
+}
+
+static void
+_LocalGetJobLevel2(PLOCAL_JOB pJob, PJOB_INFO_2W* ppJobInfo, PBYTE* ppJobInfoEnd, PDWORD pcbNeeded)
+{
+    DWORD cbDatatype;
+    DWORD cbDevMode;
+    DWORD cbDocumentName = 0;
+    DWORD cbDriverName;
+    DWORD cbMachineName;
+    DWORD cbNotifyName = 0;
+    DWORD cbPrinterName;
+    DWORD cbPrintProcessor;
+    DWORD cbPrintProcessorParameters = 0;
+    DWORD cbStatus = 0;
+    DWORD cbUserName = 0;
+    FILETIME ftNow;
+    FILETIME ftSubmitted;
+    PWSTR pwszStrings[10];
+    ULARGE_INTEGER uliNow;
+    ULARGE_INTEGER uliSubmitted;
+
+    // Calculate the string lengths.
+    cbDevMode = pJob->pDevMode->dmSize + pJob->pDevMode->dmDriverExtra;
+
+    if (!ppJobInfo)
+    {
+        cbDatatype = (wcslen(pJob->pwszDatatype) + 1) * sizeof(WCHAR);
+        cbDriverName = (wcslen(pJob->pPrinter->pwszPrinterDriver) + 1) * sizeof(WCHAR);
+        cbMachineName = (wcslen(pJob->pwszMachineName) + 1) * sizeof(WCHAR);
+        cbPrinterName = (wcslen(pJob->pPrinter->pwszPrinterName) + 1) * sizeof(WCHAR);
+        cbPrintProcessor = (wcslen(pJob->pPrintProcessor->pwszName) + 1) * sizeof(WCHAR);
+
+        // These values are optional.
+        if (pJob->pwszDocumentName)
+            cbDocumentName = (wcslen(pJob->pwszDocumentName) + 1) * sizeof(WCHAR);
+
+        if (pJob->pwszNotifyName)
+            cbNotifyName = (wcslen(pJob->pwszNotifyName) + 1) * sizeof(WCHAR);
+
+        if (pJob->pwszPrintProcessorParameters)
+            cbPrintProcessorParameters = (wcslen(pJob->pwszPrintProcessorParameters) + 1) * sizeof(WCHAR);
+
+        if (pJob->pwszStatus)
+            cbStatus = (wcslen(pJob->pwszStatus) + 1) * sizeof(WCHAR);
+
+        if (pJob->pwszUserName)
+            cbUserName = (wcslen(pJob->pwszUserName) + 1) * sizeof(WCHAR);
+
+        *pcbNeeded += sizeof(JOB_INFO_2W) + cbDatatype + cbDevMode + cbDocumentName + cbDriverName + cbMachineName + cbNotifyName + cbPrinterName + cbPrintProcessor + cbPrintProcessorParameters + cbStatus + cbUserName;
+        return;
+    }
+
+    // Set the general fields.
+    (*ppJobInfo)->JobId = pJob->dwJobID;
+    (*ppJobInfo)->Status = pJob->dwStatus;
+    (*ppJobInfo)->Priority = pJob->dwPriority;
+    (*ppJobInfo)->StartTime = pJob->dwStartTime;
+    (*ppJobInfo)->UntilTime = pJob->dwUntilTime;
+    (*ppJobInfo)->TotalPages = pJob->dwTotalPages;
+    (*ppJobInfo)->PagesPrinted = pJob->dwPagesPrinted;
+    CopyMemory(&(*ppJobInfo)->Submitted, &pJob->stSubmitted, sizeof(SYSTEMTIME));
+
+    // Time in JOB_INFO_2W is the number of milliseconds elapsed since the job was submitted. Calculate this time.
+    if (!SystemTimeToFileTime(&pJob->stSubmitted, &ftSubmitted))
+    {
+        ERR("SystemTimeToFileTime failed with error %lu!\n", GetLastError());
+        return;
+    }
+
+    GetSystemTimeAsFileTime(&ftNow);
+    uliSubmitted.LowPart = ftSubmitted.dwLowDateTime;
+    uliSubmitted.HighPart = ftSubmitted.dwHighDateTime;
+    uliNow.LowPart = ftNow.dwLowDateTime;
+    uliNow.HighPart = ftNow.dwHighDateTime;
+    (*ppJobInfo)->Time = (DWORD)((uliNow.QuadPart - uliSubmitted.QuadPart) / 10000);
+
+    // Position in JOB_INFO_2W is the 1-based index of the job in the processing queue.
+    // Retrieve this through the element index of the job in the Printer's Job List.
+    if (!LookupElementSkiplist(&pJob->pPrinter->JobList, pJob, &(*ppJobInfo)->Position))
+    {
+        ERR("pJob could not be located in the Printer's Job List!\n");
+        return;
+    }
+
+    // Make the index 1-based.
+    ++(*ppJobInfo)->Position;
+
+    // FIXME!
+    FIXME("Setting pSecurityDescriptor and Size to 0 for now!\n");
+    (*ppJobInfo)->pSecurityDescriptor = NULL;
+    (*ppJobInfo)->Size = 0;
+
+    // Set the pDevMode field (and copy the DevMode).
+    *ppJobInfoEnd -= cbDevMode;
+    CopyMemory(*ppJobInfoEnd, pJob->pDevMode, cbDevMode);
+    (*ppJobInfo)->pDevMode = (PDEVMODEW)(*ppJobInfoEnd);
+
+    // Set the pPrinterName field.
+    pwszStrings[0] = pJob->pPrinter->pwszPrinterName;
+
+    // Set the pMachineName field.
+    pwszStrings[1] = pJob->pwszMachineName;
+
+    // Set the pUserName field.
+    pwszStrings[2] = pJob->pwszUserName;
+
+    // Set the pDocument field.
+    pwszStrings[3] = pJob->pwszDocumentName;
+
+    // Set the pNotifyName field.
+    pwszStrings[4] = pJob->pwszNotifyName;
+
+    // Set the pDatatype field.
+    pwszStrings[5] = pJob->pwszDatatype;
+
+    // Set the pPrintProcessor field.
+    pwszStrings[6] = pJob->pPrintProcessor->pwszName;
+
+    // Set the pParameters field.
+    pwszStrings[7] = pJob->pwszPrintProcessorParameters;
+
+    // Set the pDriverName field.
+    pwszStrings[8] = pJob->pPrinter->pwszPrinterDriver;
+
+    // Set the pStatus field.
+    pwszStrings[9] = pJob->pwszStatus;
+
+    // Finally copy the structure and advance to the next one in the output buffer.
+    *ppJobInfoEnd = PackStrings(pwszStrings, (PBYTE)(*ppJobInfo), dwJobInfo2Offsets, *ppJobInfoEnd);
+    (*ppJobInfo)++;
+}
+
+BOOL WINAPI
+LocalGetJob(HANDLE hPrinter, DWORD JobId, DWORD Level, PBYTE pStart, DWORD cbBuf, LPDWORD pcbNeeded)
+{
+    DWORD dwErrorCode;
+    PBYTE pEnd = &pStart[cbBuf];
+    PLOCAL_HANDLE pHandle;
+    PLOCAL_JOB pJob;
+    PLOCAL_PRINTER_HANDLE pPrinterHandle;
+
+    TRACE("LocalGetJob(%p, %lu, %lu, %p, %lu, %p)\n", hPrinter, JobId, Level, pStart, cbBuf, pcbNeeded);
+
+    // Check if this is a printer handle.
+    pHandle = (PLOCAL_HANDLE)hPrinter;
+    if (pHandle->HandleType != HandleType_Printer)
+    {
+        dwErrorCode = ERROR_INVALID_HANDLE;
+        goto Cleanup;
+    }
+
+    pPrinterHandle = (PLOCAL_PRINTER_HANDLE)pHandle->pSpecificHandle;
+
+    // Get the desired job.
+    pJob = LookupElementSkiplist(&GlobalJobList, &JobId, NULL);
+    if (!pJob || pJob->pPrinter != pPrinterHandle->pPrinter)
+    {
+        dwErrorCode = ERROR_INVALID_PARAMETER;
+        goto Cleanup;
+    }
+
+    if (Level > 2)
+    {
+        // The caller supplied an invalid level for GetJob.
+        dwErrorCode = ERROR_INVALID_LEVEL;
+        goto Cleanup;
+    }
+
+    // Count the required buffer size.
+    *pcbNeeded = 0;
+
+    if (Level == 1)
+        _LocalGetJobLevel1(pJob, NULL, NULL, pcbNeeded);
+    else if (Level == 2)
+        _LocalGetJobLevel2(pJob, NULL, NULL, pcbNeeded);
+
+    // Check if the supplied buffer is large enough.
+    if (cbBuf < *pcbNeeded)
+    {
+        dwErrorCode = ERROR_INSUFFICIENT_BUFFER;
+        goto Cleanup;
+    }
+
+    // Copy over the Job information.
+    pEnd = &pStart[*pcbNeeded];
+
+    if (Level == 1)
+        _LocalGetJobLevel1(pJob, (PJOB_INFO_1W*)&pStart, &pEnd, NULL);
+    else if (Level == 2)
+        _LocalGetJobLevel2(pJob, (PJOB_INFO_2W*)&pStart, &pEnd, NULL);
+
+    dwErrorCode = ERROR_SUCCESS;
+
+Cleanup:
+    SetLastError(dwErrorCode);
+    return (dwErrorCode == ERROR_SUCCESS);
+}
+
+static DWORD
+_LocalSetJobLevel1(PLOCAL_PRINTER_HANDLE pPrinterHandle, PLOCAL_JOB pJob, PJOB_INFO_1W pJobInfo)
+{
+    DWORD dwErrorCode;
+
+    // First check the validity of the input before changing anything.
+    if (!FindDatatype(pJob->pPrintProcessor, pJobInfo->pDatatype))
+    {
+        dwErrorCode = ERROR_INVALID_DATATYPE;
+        goto Cleanup;
+    }
+
+    // Check if the datatype has changed.
+    if (!_EqualStrings(pJob->pwszDatatype, pJobInfo->pDatatype))
+    {
+        // Use the new value.
+        if (!ReallocSplStr(&pJob->pwszDatatype, pJobInfo->pDatatype))
+        {
+            dwErrorCode = ERROR_NOT_ENOUGH_MEMORY;
+            ERR("ReallocSplStr failed, last error is %lu!\n", GetLastError());
+            goto Cleanup;
+        }
+    }
+
+    // Check if the document name has changed. An empty string is permitted here!
+    if (!_EqualStrings(pJob->pwszDocumentName, pJobInfo->pDocument))
+    {
+        // Use the new value.
+        if (!ReallocSplStr(&pJob->pwszDocumentName, pJobInfo->pDocument))
+        {
+            dwErrorCode = ERROR_NOT_ENOUGH_MEMORY;
+            ERR("ReallocSplStr failed, last error is %lu!\n", GetLastError());
+            goto Cleanup;
+        }
+    }
+
+    // Check if the status message has changed. An empty string is permitted here!
+    if (!_EqualStrings(pJob->pwszStatus, pJobInfo->pStatus))
+    {
+        // Use the new value.
+        if (!ReallocSplStr(&pJob->pwszStatus, pJobInfo->pStatus))
+        {
+            dwErrorCode = ERROR_NOT_ENOUGH_MEMORY;
+            ERR("ReallocSplStr failed, last error is %lu!\n", GetLastError());
+            goto Cleanup;
+        }
+    }
+
+    // Check if the user name has changed. An empty string is permitted here!
+    if (!_EqualStrings(pJob->pwszUserName, pJobInfo->pUserName) != 0)
+    {
+        // The new user name doesn't need to exist, so no additional verification is required.
+
+        // Use the new value.
+        if (!ReallocSplStr(&pJob->pwszUserName, pJobInfo->pUserName))
+        {
+            dwErrorCode = ERROR_NOT_ENOUGH_MEMORY;
+            ERR("ReallocSplStr failed, last error is %lu!\n", GetLastError());
+            goto Cleanup;
+        }
+    }
+
+    // Check if the priority has changed.
+    if (pJob->dwPriority != pJobInfo->Priority && IS_VALID_PRIORITY(pJobInfo->Priority))
+    {
+        // Set the new priority.
+        pJob->dwPriority = pJobInfo->Priority;
+
+        // Remove and reinsert the job in the Printer's Job List.
+        // The Compare function will be used to find the right position now considering the new priority.
+        DeleteElementSkiplist(&pJob->pPrinter->JobList, pJob);
+        InsertElementSkiplist(&pJob->pPrinter->JobList, pJob);
+    }
+
+    // Check if the status flags have changed.
+    if (pJob->dwStatus != pJobInfo->Status)
+    {
+        // Only add status flags that make sense.
+        if (pJobInfo->Status & JOB_STATUS_PAUSED)
+            pJob->dwStatus |= JOB_STATUS_PAUSED;
+
+        if (pJobInfo->Status & JOB_STATUS_ERROR)
+            pJob->dwStatus |= JOB_STATUS_ERROR;
+
+        if (pJobInfo->Status & JOB_STATUS_OFFLINE)
+            pJob->dwStatus |= JOB_STATUS_OFFLINE;
+
+        if (pJobInfo->Status & JOB_STATUS_PAPEROUT)
+            pJob->dwStatus |= JOB_STATUS_PAPEROUT;
+    }
+
+    dwErrorCode = ERROR_SUCCESS;
+
+Cleanup:
+    return dwErrorCode;
+}
+
+static DWORD
+_LocalSetJobLevel2(PLOCAL_PRINTER_HANDLE pPrinterHandle, PLOCAL_JOB pJob, PJOB_INFO_2W pJobInfo)
+{
+    DWORD dwErrorCode;
+    PLOCAL_PRINT_PROCESSOR pPrintProcessor;
+
+    // First check the validity of the input before changing anything.
+    pPrintProcessor = FindPrintProcessor(pJobInfo->pPrintProcessor);
+    if (!pPrintProcessor)
+    {
+        dwErrorCode = ERROR_UNKNOWN_PRINTPROCESSOR;
+        goto Cleanup;
+    }
+
+    if (!FindDatatype(pPrintProcessor, pJobInfo->pDatatype))
+    {
+        dwErrorCode = ERROR_INVALID_DATATYPE;
+        goto Cleanup;
+    }
+
+    // Check if the datatype has changed.
+    if (!_EqualStrings(pJob->pwszDatatype, pJobInfo->pDatatype))
+    {
+        // Use the new value.
+        if (!ReallocSplStr(&pJob->pwszDatatype, pJobInfo->pDatatype))
+        {
+            dwErrorCode = ERROR_NOT_ENOUGH_MEMORY;
+            ERR("ReallocSplStr failed, last error is %lu!\n", GetLastError());
+            goto Cleanup;
+        }
+    }
+
+    // Check if the document name has changed. An empty string is permitted here!
+    if (!_EqualStrings(pJob->pwszDocumentName, pJobInfo->pDocument))
+    {
+        // Use the new value.
+        if (!ReallocSplStr(&pJob->pwszDocumentName, pJobInfo->pDocument))
+        {
+            dwErrorCode = ERROR_NOT_ENOUGH_MEMORY;
+            ERR("ReallocSplStr failed, last error is %lu!\n", GetLastError());
+            goto Cleanup;
+        }
+    }
+
+    // Check if the notify name has changed. An empty string is permitted here!
+    if (!_EqualStrings(pJob->pwszNotifyName, pJobInfo->pNotifyName))
+    {
+        // The new notify name doesn't need to exist, so no additional verification is required.
+
+        // Use the new value.
+        if (!ReallocSplStr(&pJob->pwszNotifyName, pJobInfo->pNotifyName))
+        {
+            dwErrorCode = ERROR_NOT_ENOUGH_MEMORY;
+            ERR("ReallocSplStr failed, last error is %lu!\n", GetLastError());
+            goto Cleanup;
+        }
+    }
+
+    // Check if the Print Processor Parameters have changed. An empty string is permitted here!
+    if (!_EqualStrings(pJob->pwszPrintProcessorParameters, pJobInfo->pParameters))
+    {
+        // Use the new value.
+        if (!ReallocSplStr(&pJob->pwszPrintProcessorParameters, pJobInfo->pParameters))
+        {
+            dwErrorCode = ERROR_NOT_ENOUGH_MEMORY;
+            ERR("ReallocSplStr failed, last error is %lu!\n", GetLastError());
+            goto Cleanup;
+        }
+    }
+
+    // Check if the Status Message has changed. An empty string is permitted here!
+    if (!_EqualStrings(pJob->pwszStatus, pJobInfo->pStatus))
+    {
+        // Use the new value.
+        if (!ReallocSplStr(&pJob->pwszStatus, pJobInfo->pStatus))
+        {
+            dwErrorCode = ERROR_NOT_ENOUGH_MEMORY;
+            ERR("ReallocSplStr failed, last error is %lu!\n", GetLastError());
+            goto Cleanup;
+        }
+    }
+
+    // Check if the user name has changed. An empty string is permitted here!
+    if (!_EqualStrings(pJob->pwszUserName, pJobInfo->pUserName))
+    {
+        // The new user name doesn't need to exist, so no additional verification is required.
+
+        // Use the new value.
+        if (!ReallocSplStr(&pJob->pwszUserName, pJobInfo->pUserName))
+        {
+            dwErrorCode = ERROR_NOT_ENOUGH_MEMORY;
+            ERR("ReallocSplStr failed, last error is %lu!\n", GetLastError());
+            goto Cleanup;
+        }
+    }
+
+    // Check if the priority has changed.
+    if (pJob->dwPriority != pJobInfo->Priority && IS_VALID_PRIORITY(pJobInfo->Priority))
+    {
+        // Set the new priority.
+        pJob->dwPriority = pJobInfo->Priority;
+
+        // Remove and reinsert the job in the Printer's Job List.
+        // The Compare function will be used to find the right position now considering the new priority.
+        DeleteElementSkiplist(&pJob->pPrinter->JobList, pJob);
+        InsertElementSkiplist(&pJob->pPrinter->JobList, pJob);
+    }
+
+    // Check if the status flags have changed.
+    if (pJob->dwStatus != pJobInfo->Status)
+    {
+        // Only add status flags that make sense.
+        if (pJobInfo->Status & JOB_STATUS_PAUSED)
+            pJob->dwStatus |= JOB_STATUS_PAUSED;
+
+        if (pJobInfo->Status & JOB_STATUS_ERROR)
+            pJob->dwStatus |= JOB_STATUS_ERROR;
+
+        if (pJobInfo->Status & JOB_STATUS_OFFLINE)
+            pJob->dwStatus |= JOB_STATUS_OFFLINE;
+
+        if (pJobInfo->Status & JOB_STATUS_PAPEROUT)
+            pJob->dwStatus |= JOB_STATUS_PAPEROUT;
+    }
+
+    dwErrorCode = ERROR_SUCCESS;
+
+Cleanup:
+    return dwErrorCode;
+}
+
+BOOL WINAPI
+LocalSetJob(HANDLE hPrinter, DWORD JobId, DWORD Level, PBYTE pJobInfo, DWORD Command)
+{
+    DWORD dwErrorCode;
+    PLOCAL_HANDLE pHandle;
+    PLOCAL_JOB pJob;
+    PLOCAL_PRINTER_HANDLE pPrinterHandle;
+    WCHAR wszFullPath[MAX_PATH];
+
+    TRACE("LocalSetJob(%p, %lu, %lu, %p, %lu)\n", hPrinter, JobId, Level, pJobInfo, Command);
+
+    // Check if this is a printer handle.
+    pHandle = (PLOCAL_HANDLE)hPrinter;
+    if (pHandle->HandleType != HandleType_Printer)
+    {
+        dwErrorCode = ERROR_INVALID_HANDLE;
+        goto Cleanup;
+    }
+
+    pPrinterHandle = (PLOCAL_PRINTER_HANDLE)pHandle->pSpecificHandle;
+
+    // Get the desired job.
+    pJob = LookupElementSkiplist(&GlobalJobList, &JobId, NULL);
+    if (!pJob || pJob->pPrinter != pPrinterHandle->pPrinter)
+    {
+        dwErrorCode = ERROR_INVALID_PARAMETER;
+        goto Cleanup;
+    }
+
+    // Setting job information is handled differently for each level.
+    if (Level)
+    {
+        if (Level == 1)
+            dwErrorCode = _LocalSetJobLevel1(pPrinterHandle, pJob, (PJOB_INFO_1W)pJobInfo);
+        else if (Level == 2)
+            dwErrorCode = _LocalSetJobLevel2(pPrinterHandle, pJob, (PJOB_INFO_2W)pJobInfo);
+        else
+            dwErrorCode = ERROR_INVALID_LEVEL;
+    }
+
+    if (dwErrorCode != ERROR_SUCCESS)
+        goto Cleanup;
+
+    // If we do spooled printing, the job information is written down into a shadow file.
+    if (!(pPrinterHandle->pPrinter->dwAttributes & PRINTER_ATTRIBUTE_DIRECT))
+    {
+        // Write the job data into the shadow file.
+        GetJobFilePath(L"SHD", JobId, wszFullPath);
+        WriteJobShadowFile(wszFullPath, pJob);
+    }
+
+    // Perform an additional command if desired.
+    if (Command)
+    {
+        if (Command == JOB_CONTROL_SENT_TO_PRINTER)
+        {
+            // This indicates the end of the Print Job.
+
+            // Cancel the Job at the Print Processor.
+            if (pJob->hPrintProcessor)
+                pJob->pPrintProcessor->pfnControlPrintProcessor(pJob->hPrintProcessor, JOB_CONTROL_CANCEL);
+
+            FreeJob(pJob);
+
+            // TODO: All open handles associated with the job need to be invalidated.
+            // This certainly needs handle tracking...
+        }
+        else
+        {
+            ERR("Unimplemented SetJob Command: %lu!\n", Command);
+        }
+    }
+
+    dwErrorCode = ERROR_SUCCESS;
+
+Cleanup:
+    SetLastError(dwErrorCode);
+    return (dwErrorCode == ERROR_SUCCESS);
+}
+
+BOOL WINAPI
+LocalEnumJobs(HANDLE hPrinter, DWORD FirstJob, DWORD NoJobs, DWORD Level, PBYTE pStart, DWORD cbBuf, LPDWORD pcbNeeded, LPDWORD pcReturned)
+{
+    DWORD dwErrorCode;
+    DWORD i;
+    PBYTE pEnd;
+    PLOCAL_HANDLE pHandle;
+    PLOCAL_JOB pJob;
+    PSKIPLIST_NODE pFirstJobNode;
+    PSKIPLIST_NODE pNode;
+    PLOCAL_PRINTER_HANDLE pPrinterHandle;
+
+    TRACE("LocalEnumJobs(%p, %lu, %lu, %lu, %p, %lu, %p, %p)\n", hPrinter, FirstJob, NoJobs, Level, pStart, cbBuf, pcbNeeded, pcReturned);
+
+    // Check if this is a printer handle.
+    pHandle = (PLOCAL_HANDLE)hPrinter;
+    if (pHandle->HandleType != HandleType_Printer)
+    {
+        dwErrorCode = ERROR_INVALID_HANDLE;
+        goto Cleanup;
+    }
+
+    pPrinterHandle = (PLOCAL_PRINTER_HANDLE)pHandle->pSpecificHandle;
+
+    // Check the level.
+    if (Level > 2)
+    {
+        dwErrorCode = ERROR_INVALID_LEVEL;
+        goto Cleanup;
+    }
+
+    // Begin counting.
+    *pcbNeeded = 0;
+    *pcReturned = 0;
+
+    // Lookup the node of the first job requested by the caller in the Printer's Job List.
+    pFirstJobNode = LookupNodeByIndexSkiplist(&pPrinterHandle->pPrinter->JobList, FirstJob);
+
+    // Count the required buffer size and the number of jobs.
+    i = 0;
+    pNode = pFirstJobNode;
+
+    while (i < NoJobs && pNode)
+    {
+        pJob = (PLOCAL_JOB)pNode->Element;
+
+        if (Level == 1)
+            _LocalGetJobLevel1(pJob, NULL, NULL, pcbNeeded);
+        else if (Level == 2)
+            _LocalGetJobLevel2(pJob, NULL, NULL, pcbNeeded);
+
+        // We stop either when there are no more jobs in the list or when the caller didn't request more, whatever comes first.
+        i++;
+        pNode = pNode->Next[0];
+    }
+
+    // Check if the supplied buffer is large enough.
+    if (cbBuf < *pcbNeeded)
+    {
+        dwErrorCode = ERROR_INSUFFICIENT_BUFFER;
+        goto Cleanup;
+    }
+
+    // Copy over the Job information.
+    i = 0;
+    pNode = pFirstJobNode;
+    pEnd = &pStart[*pcbNeeded];
+
+    while (i < NoJobs && pNode)
+    {
+        pJob = (PLOCAL_JOB)pNode->Element;
+
+        if (Level == 1)
+            _LocalGetJobLevel1(pJob, (PJOB_INFO_1W*)&pStart, &pEnd, NULL);
+        else if (Level == 2)
+            _LocalGetJobLevel2(pJob, (PJOB_INFO_2W*)&pStart, &pEnd, NULL);
+
+        // We stop either when there are no more jobs in the list or when the caller didn't request more, whatever comes first.
+        i++;
+        pNode = pNode->Next[0];
+    }
+
+    *pcReturned = i;
+    dwErrorCode = ERROR_SUCCESS;
+
+Cleanup:
+    SetLastError(dwErrorCode);
+    return (dwErrorCode == ERROR_SUCCESS);
+}
+
+BOOL WINAPI
+LocalScheduleJob(HANDLE hPrinter, DWORD dwJobID)
+{
+    DWORD dwAttributes;
+    DWORD dwErrorCode;
+    HANDLE hThread;
+    PLOCAL_JOB pJob;
+    PLOCAL_HANDLE pHandle = (PLOCAL_HANDLE)hPrinter;
+    PLOCAL_PRINTER_HANDLE pPrinterHandle;
+    WCHAR wszFullPath[MAX_PATH];
+
+    TRACE("LocalScheduleJob(%p, %lu)\n", hPrinter, dwJobID);
+
+    // Check if this is a printer handle.
+    if (pHandle->HandleType != HandleType_Printer)
+    {
+        dwErrorCode = ERROR_INVALID_HANDLE;
+        goto Cleanup;
+    }
+
+    pPrinterHandle = (PLOCAL_PRINTER_HANDLE)pHandle->pSpecificHandle;
+
+    // Check if the Job ID is valid.
+    pJob = LookupElementSkiplist(&GlobalJobList, &dwJobID, NULL);
+    if (!pJob || pJob->pPrinter != pPrinterHandle->pPrinter)
+    {
+        dwErrorCode = ERROR_INVALID_PARAMETER;
+        goto Cleanup;
+    }
+
+    // Check if this Job was started with AddJob.
+    if (!pJob->bAddedJob)
+    {
+        dwErrorCode = ERROR_SPL_NO_ADDJOB;
+        goto Cleanup;
+    }
+
+    // Construct the full path to the spool file.
+    GetJobFilePath(L"SPL", dwJobID, wszFullPath);
+
+    // Check if it exists.
+    dwAttributes = GetFileAttributesW(wszFullPath);
+    if (dwAttributes == INVALID_FILE_ATTRIBUTES || dwAttributes & FILE_ATTRIBUTE_DIRECTORY)
+    {
+        dwErrorCode = ERROR_SPOOL_FILE_NOT_FOUND;
+        goto Cleanup;
+    }
+
+    // Spooling is finished at this point.
+    pJob->dwStatus &= ~JOB_STATUS_SPOOLING;
+
+    // Write the job data into the shadow file.
+    wcscpy(wcsrchr(wszFullPath, L'.'), L".SHD");
+    WriteJobShadowFile(wszFullPath, pJob);
+
+    // Create the thread for performing the printing process.
+    hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)PrintingThreadProc, pJob, 0, NULL);
+    if (!hThread)
+    {
+        dwErrorCode = GetLastError();
+        ERR("CreateThread failed with error %lu!\n", dwErrorCode);
+        goto Cleanup;
+    }
+
+    // We don't need the thread handle. Keeping it open blocks the thread from terminating.
+    CloseHandle(hThread);
+
+    // ScheduleJob has done its job. The rest happens inside the thread.
+    dwErrorCode = ERROR_SUCCESS;
+
+Cleanup:
+    SetLastError(dwErrorCode);
+    return (dwErrorCode == ERROR_SUCCESS);
+}
+
+PLOCAL_JOB
+ReadJobShadowFile(PCWSTR pwszFilePath)
+{
+    DWORD cbFileSize;
+    DWORD cbRead;
+    HANDLE hFile = INVALID_HANDLE_VALUE;
+    PLOCAL_JOB pJob;
+    PLOCAL_JOB pReturnValue = NULL;
+    PLOCAL_PRINTER pPrinter;
+    PLOCAL_PRINT_PROCESSOR pPrintProcessor;
+    PSHD_HEADER pShadowFile = NULL;
+    PWSTR pwszPrinterName;
+    PWSTR pwszPrintProcessor;
+
+    TRACE("ReadJobShadowFile(%S)\n", pwszFilePath);
+
+    // Try to open the file.
+    hFile = CreateFileW(pwszFilePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
+    if (hFile == INVALID_HANDLE_VALUE)
+    {
+        ERR("CreateFileW failed with error %lu for file \"%S\"!\n", GetLastError(), pwszFilePath);
+        goto Cleanup;
+    }
+
+    // Get its file size (small enough for a single DWORD) and allocate memory for all of it.
+    cbFileSize = GetFileSize(hFile, NULL);
+    pShadowFile = DllAllocSplMem(cbFileSize);
+    if (!pShadowFile)
+    {
+        ERR("DllAllocSplMem failed for file \"%S\"!\n", pwszFilePath);
+        goto Cleanup;
+    }
+
+    // Read the entire file.
+    if (!ReadFile(hFile, pShadowFile, cbFileSize, &cbRead, NULL))
+    {
+        ERR("ReadFile failed with error %lu for file \"%S\"!\n", GetLastError(), pwszFilePath);
+        goto Cleanup;
+    }
+
+    // Check signature and header size.
+    if (pShadowFile->dwSignature != SHD_WIN2003_SIGNATURE || pShadowFile->cbHeader != sizeof(SHD_HEADER))
+    {
+        ERR("Signature or Header Size mismatch for file \"%S\"!\n", pwszFilePath);
+        goto Cleanup;
+    }
+
+    // Retrieve the associated printer from the list.
+    pwszPrinterName = (PWSTR)((ULONG_PTR)pShadowFile + pShadowFile->offPrinterName);
+    pPrinter = LookupElementSkiplist(&PrinterList, &pwszPrinterName, NULL);
+    if (!pPrinter)
+    {
+        ERR("Shadow file \"%S\" references a non-existing printer \"%S\"!\n", pwszFilePath, pwszPrinterName);
+        goto Cleanup;
+    }
+
+    // Retrieve the associated Print Processor from the list.
+    pwszPrintProcessor = (PWSTR)((ULONG_PTR)pShadowFile + pShadowFile->offPrintProcessor);
+    pPrintProcessor = FindPrintProcessor(pwszPrintProcessor);
+    if (!pPrintProcessor)
+    {
+        ERR("Shadow file \"%S\" references a non-existing Print Processor \"%S\"!\n", pwszFilePath, pwszPrintProcessor);
+        goto Cleanup;
+    }
+
+    // Create a new job structure and copy over the relevant fields.
+    pJob = DllAllocSplMem(sizeof(LOCAL_JOB));
+    if (!pJob)
+    {
+        ERR("DllAllocSplMem failed for file \"%S\"!\n", pwszFilePath);
+        goto Cleanup;
+    }
+
+    pJob->dwJobID = pShadowFile->dwJobID;
+    pJob->dwPriority = pShadowFile->dwPriority;
+    pJob->dwStartTime = pShadowFile->dwStartTime;
+    pJob->dwTotalPages = pShadowFile->dwTotalPages;
+    pJob->dwUntilTime = pShadowFile->dwUntilTime;
+    pJob->pPrinter = pPrinter;
+    pJob->pPrintProcessor = pPrintProcessor;
+    pJob->pDevMode = DuplicateDevMode((PDEVMODEW)((ULONG_PTR)pShadowFile + pShadowFile->offDevMode));
+    pJob->pwszDatatype = AllocSplStr((PCWSTR)((ULONG_PTR)pShadowFile + pShadowFile->offDatatype));
+    pJob->pwszMachineName = AllocSplStr((PCWSTR)((ULONG_PTR)pShadowFile + pShadowFile->offMachineName));
+    CopyMemory(&pJob->stSubmitted, &pShadowFile->stSubmitted, sizeof(SYSTEMTIME));
+
+    // Copy the optional values.
+    if (pShadowFile->offDocumentName)
+        pJob->pwszDocumentName = AllocSplStr((PCWSTR)((ULONG_PTR)pShadowFile + pShadowFile->offDocumentName));
+
+    if (pShadowFile->offNotifyName)
+        pJob->pwszNotifyName = AllocSplStr((PCWSTR)((ULONG_PTR)pShadowFile + pShadowFile->offNotifyName));
+
+    if (pShadowFile->offPrintProcessorParameters)
+        pJob->pwszPrintProcessorParameters = AllocSplStr((PCWSTR)((ULONG_PTR)pShadowFile + pShadowFile->offPrintProcessorParameters));
+
+    if (pShadowFile->offUserName)
+        pJob->pwszUserName = AllocSplStr((PCWSTR)((ULONG_PTR)pShadowFile + pShadowFile->offUserName));
+
+    // Jobs read from shadow files were always added using AddJob.
+    pJob->bAddedJob = TRUE;
+
+    pReturnValue = pJob;
+
+Cleanup:
+    if (pShadowFile)
+        DllFreeSplMem(pShadowFile);
+
+    if (hFile != INVALID_HANDLE_VALUE)
+        CloseHandle(hFile);
+
+    return pReturnValue;
+}
+
+BOOL
+WriteJobShadowFile(PWSTR pwszFilePath, const PLOCAL_JOB pJob)
+{
+    BOOL bReturnValue = FALSE;
+    DWORD cbDatatype = (wcslen(pJob->pwszDatatype) + 1) * sizeof(WCHAR);
+    DWORD cbDevMode = pJob->pDevMode->dmSize + pJob->pDevMode->dmDriverExtra;
+    DWORD cbDocumentName = 0;
+    DWORD cbFileSize;
+    DWORD cbMachineName = (wcslen(pJob->pwszMachineName) + 1) * sizeof(WCHAR);
+    DWORD cbNotifyName = 0;
+    DWORD cbPrinterDriver = (wcslen(pJob->pPrinter->pwszPrinterDriver) + 1) * sizeof(WCHAR);
+    DWORD cbPrinterName = (wcslen(pJob->pPrinter->pwszPrinterName) + 1) * sizeof(WCHAR);
+    DWORD cbPrintProcessor = (wcslen(pJob->pPrintProcessor->pwszName) + 1) * sizeof(WCHAR);
+    DWORD cbPrintProcessorParameters = 0;
+    DWORD cbUserName = 0;
+    DWORD cbWritten;
+    DWORD dwCurrentOffset;
+    HANDLE hSHDFile = INVALID_HANDLE_VALUE;
+    HANDLE hSPLFile = INVALID_HANDLE_VALUE;
+    PSHD_HEADER pShadowFile = NULL;
+
+    TRACE("WriteJobShadowFile(%S, %p)\n", pwszFilePath, pJob);
+
+    // Try to open the SHD file.
+    hSHDFile = CreateFileW(pwszFilePath, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, 0, NULL);
+    if (hSHDFile == INVALID_HANDLE_VALUE)
+    {
+        ERR("CreateFileW failed with error %lu for file \"%S\"!\n", GetLastError(), pwszFilePath);
+        goto Cleanup;
+    }
+
+    // Calculate the lengths of the optional values and the total size of the shadow file.
+    if (pJob->pwszDocumentName)
+        cbDocumentName = (wcslen(pJob->pwszDocumentName) + 1) * sizeof(WCHAR);
+
+    if (pJob->pwszNotifyName)
+        cbNotifyName = (wcslen(pJob->pwszNotifyName) + 1) * sizeof(WCHAR);
+
+    if (pJob->pwszPrintProcessorParameters)
+        cbPrintProcessorParameters = (wcslen(pJob->pwszPrintProcessorParameters) + 1) * sizeof(WCHAR);
+
+    if (pJob->pwszUserName)
+        cbUserName = (wcslen(pJob->pwszUserName) + 1) * sizeof(WCHAR);
+
+    cbFileSize = sizeof(SHD_HEADER) + cbDatatype + cbDocumentName + cbDevMode + cbMachineName + cbNotifyName + cbPrinterDriver + cbPrinterName + cbPrintProcessor + cbPrintProcessorParameters + cbUserName;
+
+    // Allocate memory for it.
+    pShadowFile = DllAllocSplMem(cbFileSize);
+    if (!pShadowFile)
+    {
+        ERR("DllAllocSplMem failed for file \"%S\"!\n", pwszFilePath);
+        goto Cleanup;
+    }
+
+    // Fill out the shadow file header information.
+    pShadowFile->dwSignature = SHD_WIN2003_SIGNATURE;
+    pShadowFile->cbHeader = sizeof(SHD_HEADER);
+
+    // Copy the values.
+    pShadowFile->dwJobID = pJob->dwJobID;
+    pShadowFile->dwPriority = pJob->dwPriority;
+    pShadowFile->dwStartTime = pJob->dwStartTime;
+    pShadowFile->dwTotalPages = pJob->dwTotalPages;
+    pShadowFile->dwUntilTime = pJob->dwUntilTime;
+    CopyMemory(&pShadowFile->stSubmitted, &pJob->stSubmitted, sizeof(SYSTEMTIME));
+
+    // Determine the file size of the .SPL file
+    wcscpy(wcsrchr(pwszFilePath, L'.'), L".SPL");
+    hSPLFile = CreateFileW(pwszFilePath, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
+    if (hSPLFile != INVALID_HANDLE_VALUE)
+        pShadowFile->dwSPLSize = GetFileSize(hSPLFile, NULL);
+
+    // Add the extra values that are stored as offsets in the shadow file.
+    // The first value begins right after the shadow file header.
+    dwCurrentOffset = sizeof(SHD_HEADER);
+
+    CopyMemory((PBYTE)pShadowFile + dwCurrentOffset, pJob->pwszDatatype, cbDatatype);
+    pShadowFile->offDatatype = dwCurrentOffset;
+    dwCurrentOffset += cbDatatype;
+
+    CopyMemory((PBYTE)pShadowFile + dwCurrentOffset, pJob->pDevMode, cbDevMode);
+    pShadowFile->offDevMode = dwCurrentOffset;
+    dwCurrentOffset += cbDevMode;
+
+    // offDriverName is only written, but automatically determined through offPrinterName when reading.
+    CopyMemory((PBYTE)pShadowFile + dwCurrentOffset, pJob->pPrinter->pwszPrinterDriver, cbPrinterDriver);
+    pShadowFile->offDriverName = dwCurrentOffset;
+    dwCurrentOffset += cbPrinterDriver;
+
+    CopyMemory((PBYTE)pShadowFile + dwCurrentOffset, pJob->pwszMachineName, cbMachineName);
+    pShadowFile->offMachineName = dwCurrentOffset;
+    dwCurrentOffset += cbMachineName;
+
+    CopyMemory((PBYTE)pShadowFile + dwCurrentOffset, pJob->pPrinter->pwszPrinterName, cbPrinterName);
+    pShadowFile->offPrinterName = dwCurrentOffset;
+    dwCurrentOffset += cbPrinterName;
+
+    CopyMemory((PBYTE)pShadowFile + dwCurrentOffset, pJob->pPrintProcessor->pwszName, cbPrintProcessor);
+    pShadowFile->offPrintProcessor = dwCurrentOffset;
+    dwCurrentOffset += cbPrintProcessor;
+
+    // Copy the optional values.
+    if (cbDocumentName)
+    {
+        CopyMemory((PBYTE)pShadowFile + dwCurrentOffset, pJob->pwszDocumentName, cbDocumentName);
+        pShadowFile->offDocumentName = dwCurrentOffset;
+        dwCurrentOffset += cbDocumentName;
+    }
+
+    if (cbNotifyName)
+    {
+        CopyMemory((PBYTE)pShadowFile + dwCurrentOffset, pJob->pwszNotifyName, cbNotifyName);
+        pShadowFile->offNotifyName = dwCurrentOffset;
+        dwCurrentOffset += cbNotifyName;
+    }
+
+    if (cbPrintProcessorParameters)
+    {
+        CopyMemory((PBYTE)pShadowFile + dwCurrentOffset, pJob->pwszPrintProcessorParameters, cbPrintProcessorParameters);
+        pShadowFile->offPrintProcessorParameters = dwCurrentOffset;
+        dwCurrentOffset += cbPrintProcessorParameters;
+    }
+
+    if (cbUserName)
+    {
+        CopyMemory((PBYTE)pShadowFile + dwCurrentOffset, pJob->pwszUserName, cbUserName);
+        pShadowFile->offUserName = dwCurrentOffset;
+        dwCurrentOffset += cbUserName;
+    }
+
+    // Write the file.
+    if (!WriteFile(hSHDFile, pShadowFile, cbFileSize, &cbWritten, NULL))
+    {
+        ERR("WriteFile failed with error %lu for file \"%S\"!\n", GetLastError(), pwszFilePath);
+        goto Cleanup;
+    }
+
+    bReturnValue = TRUE;
+
+Cleanup:
+    if (pShadowFile)
+        DllFreeSplMem(pShadowFile);
+
+    if (hSHDFile != INVALID_HANDLE_VALUE)
+        CloseHandle(hSHDFile);
+
+    if (hSPLFile != INVALID_HANDLE_VALUE)
+        CloseHandle(hSPLFile);
+
+    return bReturnValue;
+}
+
+void
+FreeJob(PLOCAL_JOB pJob)
+{
+    PWSTR pwszSHDFile;
+
+    TRACE("FreeJob(%p)\n", pJob);
+
+    // Remove the Job from both Job Lists.
+    DeleteElementSkiplist(&pJob->pPrinter->JobList, pJob);
+    DeleteElementSkiplist(&GlobalJobList, pJob);
+
+    // Try to delete the corresponding .SHD file.
+    pwszSHDFile = DllAllocSplMem(GetJobFilePath(L"SHD", 0, NULL));
+    if (pwszSHDFile && GetJobFilePath(L"SHD", pJob->dwJobID, pwszSHDFile))
+        DeleteFileW(pwszSHDFile);
+
+    // Free memory for the mandatory fields.
+    DllFreeSplMem(pJob->pDevMode);
+    DllFreeSplStr(pJob->pwszDatatype);
+    DllFreeSplStr(pJob->pwszDocumentName);
+    DllFreeSplStr(pJob->pwszMachineName);
+    DllFreeSplStr(pJob->pwszNotifyName);
+    DllFreeSplStr(pJob->pwszUserName);
+
+    // Free memory for the optional fields if they are present.
+    if (pJob->pwszOutputFile)
+        DllFreeSplStr(pJob->pwszOutputFile);
+    
+    if (pJob->pwszPrintProcessorParameters)
+        DllFreeSplStr(pJob->pwszPrintProcessorParameters);
+
+    if (pJob->pwszStatus)
+        DllFreeSplStr(pJob->pwszStatus);
+
+    // Finally free the job structure itself.
+    DllFreeSplMem(pJob);
+}