diff --git a/Makefile b/Makefile index 4f589b2..50a20f5 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,7 @@ XBE_XTIMAGE = $(CURDIR)/resources/logo.xpr # SRCS += \ $(CURDIR)/source/attach.c \ + $(CURDIR)/source/helpers.c \ $(CURDIR)/source/resources.packed.obj CFLAGS += \ diff --git a/source/attach.c b/source/attach.c index df27606..04c622f 100644 --- a/source/attach.c +++ b/source/attach.c @@ -2,199 +2,188 @@ // SPDX-FileCopyrightText: 2023 Dustin Holden // SPDX-License-Identifier: GPL-2.0 // Based on attach.c from driveimageutils by rmenhal -#include #include #include +#include +#include +#include #include "attach.h" +#include "helpers.h" + +// Device name +ANSI_STRING VirtualDeviceName = RTL_CONSTANT_STRING("\\Device\\CdRom1"); + +// Virtual disc image slice data +CHAR VirtualFilePath[8][MAX_PATH] = { 0 }; + +ATTACH_SLICE_DATA AttachSliceData = { + .NumberOfSlices = 0, + .Files = { + { .Buffer = VirtualFilePath[0], .MaximumLength = sizeof(VirtualFilePath[0]) }, + { .Buffer = VirtualFilePath[1], .MaximumLength = sizeof(VirtualFilePath[1]) }, + { .Buffer = VirtualFilePath[2], .MaximumLength = sizeof(VirtualFilePath[2]) }, + { .Buffer = VirtualFilePath[3], .MaximumLength = sizeof(VirtualFilePath[3]) }, + { .Buffer = VirtualFilePath[4], .MaximumLength = sizeof(VirtualFilePath[4]) }, + { .Buffer = VirtualFilePath[5], .MaximumLength = sizeof(VirtualFilePath[5]) }, + { .Buffer = VirtualFilePath[6], .MaximumLength = sizeof(VirtualFilePath[6]) }, + { .Buffer = VirtualFilePath[7], .MaximumLength = sizeof(VirtualFilePath[7]) }, + } +}; -#define MAX_PATHNAME 256 - -int has_iso_extension(unsigned int len, char *str) { - ANSI_STRING tail; - char extension[] = ".iso"; - ANSI_STRING ext_str = { sizeof(extension) - 1, sizeof(extension), extension }; - - if(len < sizeof(extension) - 1) - return FALSE; - - tail.Length = sizeof(extension) - 1; - tail.MaximumLength = tail.Length; - tail.Buffer = str + len - sizeof(extension) + 1; - - return (RtlCompareString(&tail, &ext_str, TRUE) == 0); -} - -int has_cso_extension(unsigned int len, char *str) { - ANSI_STRING tail; - char extension[] = ".cso"; - ANSI_STRING ext_str = { sizeof(extension) - 1, sizeof(extension), extension }; - - if(len < sizeof(extension) - 1) - return FALSE; - - tail.Length = sizeof(extension) - 1; - tail.MaximumLength = tail.Length; - tail.Buffer = str + len - sizeof(extension) + 1; - - return (RtlCompareString(&tail, &ext_str, TRUE) == 0); -} - -int compare_string_tails(PANSI_STRING str1, PANSI_STRING str2, WORD skip) { - ANSI_STRING n1, n2; - - if(str1->Length <= skip) - return(str2->Length <= skip) ? 0 : -1; - else if(str2->Length <= skip) - return 1; - - n1.Length = str1->Length - skip; - n1.MaximumLength = n1.Length; - n1.Buffer = str1->Buffer + skip; - - n2.Length = str2->Length - skip; - n2.MaximumLength = n2.Length; - n2.Buffer = str2->Buffer + skip; +int main(void) __attribute__((optnone)) { + // Make a copy of the XBE launch path. + ANSI_STRING SearchPath = *XeImageFileName; - return RtlCompareString(&n1, &n2, TRUE); -} + // Determine the last backslash in the search path. + PCHAR LastBackslash = StrRChr(&SearchPath, '\\'); -int main(void) { - char info_buf[sizeof(FILE_DIRECTORY_INFORMATION) + MAX_PATHNAME]; + // Check if the last backslash was found. + if(LastBackslash == NULL) + goto CleanupAndExit; - char path[MAX_PATHNAME] = { 0 }; - strncpy(path, XeImageFileName->Buffer, - XeImageFileName->Length < (MAX_PATHNAME - 1) ? XeImageFileName->Length : (MAX_PATHNAME - 1)); + // Update the search path to only include the full device path. + SearchPath.Length = (USHORT)(LastBackslash - SearchPath.Buffer + 1); - int pathlen = strrchr(path, '\\') - path + 1; - path[pathlen] = '\0'; + // Sanity check the length of the search path. + if(SearchPath.Length < sizeof("\\Device\\") || SearchPath.Length > (MAX_PATH - 4)) + goto CleanupAndExit; - ANSI_STRING dir_name; - RtlInitAnsiString(&dir_name, path); - - OBJECT_ATTRIBUTES obj_attr = { - .RootDirectory = NULL, - .ObjectName = &dir_name, + // Open the directory. + OBJECT_ATTRIBUTES ObjectAttributes = { + .ObjectName = &SearchPath, .Attributes = OBJ_CASE_INSENSITIVE, }; - IO_STATUS_BLOCK io_status; - HANDLE h; - NTSTATUS status = NtOpenFile(&h, GENERIC_READ | SYNCHRONIZE, &obj_attr, &io_status, - FILE_SHARE_READ, FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT); - - if(!NT_SUCCESS(status)) - HalReturnToFirmware(HalQuickRebootRoutine); + IO_STATUS_BLOCK IoStatusBlock; + HANDLE Handle; - void *membuf = NULL; - unsigned long membuf_size = 1024 * 1024; + NTSTATUS Status = NtOpenFile(&Handle, FILE_LIST_DIRECTORY | SYNCHRONIZE, &ObjectAttributes, &IoStatusBlock, + FILE_SHARE_READ, FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT); - status = NtAllocateVirtualMemory(&membuf, 0, &membuf_size, MEM_COMMIT | MEM_NOZERO, PAGE_READWRITE); + if(!NT_SUCCESS(Status)) + goto CleanupAndExit; - if(!NT_SUCCESS(status)) - HalReturnToFirmware(HalQuickRebootRoutine); + // Query the directory for the first file. + // TODO: Can we use the FileName mask here to filter out only the files we're interested in? + FileInfo FileInformation = { 0 }; + Status = NtQueryDirectoryFile(Handle, NULL, NULL, NULL, &IoStatusBlock, &FileInformation, sizeof(FileInformation), + FileDirectoryInformation, NULL, TRUE); - ATTACH_SLICE_DATA *asd = (ATTACH_SLICE_DATA *)membuf; - asd->num_slices = 0; + // Loop through all files in the directory. + while(TRUE) { + // Check if we've reached the end of the directory. + if(Status == STATUS_NO_MORE_FILES) + break; - char *strbuf = (char *)membuf + sizeof(ATTACH_SLICE_DATA); + // Check if an error occurred. + if(!NT_SUCCESS(Status)) { + NtClose(Handle); + goto CleanupAndExit; + } - PFILE_DIRECTORY_INFORMATION dir_info; - BOOLEAN first = TRUE; - for(;;) { - if(first || dir_info->NextEntryOffset == 0) { - status = NtQueryDirectoryFile(h, NULL, NULL, NULL, &io_status, - info_buf, sizeof(info_buf), FileDirectoryInformation, NULL, first); + // Skip directories. + if(FileInformation.DirectoryInfo.FileAttributes & FILE_ATTRIBUTE_DIRECTORY) + goto QueryNextFile; - if(status == STATUS_NO_MORE_FILES) - break; - - first = FALSE; - dir_info = (PFILE_DIRECTORY_INFORMATION)info_buf; - } else - dir_info = (PFILE_DIRECTORY_INFORMATION)((char *)dir_info + dir_info->NextEntryOffset); + // Null-terminate the file name. + ANSI_STRING FileName = { + .Length = FileInformation.DirectoryInfo.FileNameLength, + .MaximumLength = FileInformation.DirectoryInfo.FileNameLength, + .Buffer = FileInformation.DirectoryInfo.FileName, + }; - if(!has_iso_extension(dir_info->FileNameLength, dir_info->FileName) && - !has_cso_extension(dir_info->FileNameLength, dir_info->FileName)) - continue; + // Check if the file is a supported virtual disc image. + if(!IsFileDiscImage(&FileName)) + goto QueryNextFile; - ANSI_STRING new_file = { - .Length = pathlen + dir_info->FileNameLength, - .MaximumLength = new_file.Length, - .Buffer = strbuf + // Build the slice path string by concatenating the search path and the file name. + ANSI_STRING SlicePath = { + .Length = 0, + .MaximumLength = sizeof(VirtualFilePath[0]), + .Buffer = VirtualFilePath[AttachSliceData.NumberOfSlices] }; - memcpy(strbuf, path, pathlen); - memcpy(strbuf + pathlen, dir_info->FileName, dir_info->FileNameLength); - strbuf += new_file.Length; + // NOTE: We've already checked that the search path string is < MAX_PATH, so no need to check it here. + RtlCopyString(&SlicePath, &SearchPath); - int i; - for(i = 0; i < asd->num_slices; i++) { - if(compare_string_tails(&new_file, &asd->slice_files[i], pathlen) < 0) - break; + Status = RtlAppendStringToString(&SlicePath, &FileName); + if(!NT_SUCCESS(Status)) { + NtClose(Handle); + goto CleanupAndExit; } - RtlMoveMemory(&asd->slice_files[i] + 1, &asd->slice_files[i], (asd->num_slices - i) * sizeof(ANSI_STRING)); + // Determine the slice index so the slices are stored in alphabetical order. + DWORD Index; + for(Index = 0; Index < AttachSliceData.NumberOfSlices; Index++) { + if(CompareStringTails(&SlicePath, &AttachSliceData.Files[Index], SearchPath.Length) < 0) + break; + } - asd->slice_files[i] = new_file; + // Move the slice data so the new slice can be inserted at the correct position. + RtlMoveMemory(&AttachSliceData.Files[Index] + 1, &AttachSliceData.Files[Index], + (AttachSliceData.NumberOfSlices - Index) * sizeof(ANSI_STRING)); - if(++asd->num_slices >= MAX_IMAGE_SLICES) - break; - } + // Store the slice data. + AttachSliceData.Files[Index] = SlicePath; - NtClose(h); + // Increment the number of slices. + AttachSliceData.NumberOfSlices++; - ANSI_STRING dev_name; - RtlInitAnsiString(&dev_name, "\\Device\\CdRom1"); + // Check if we've reached the maximum number of slices. + if(AttachSliceData.NumberOfSlices >= MAX_IMAGE_SLICES) + break; - obj_attr.RootDirectory = NULL; - obj_attr.ObjectName = &dev_name; - obj_attr.Attributes = OBJ_CASE_INSENSITIVE; +QueryNextFile: + RtlZeroMemory(&FileInformation, sizeof(FileInformation)); + Status = NtQueryDirectoryFile(Handle, NULL, NULL, NULL, &IoStatusBlock, &FileInformation, + sizeof(FileInformation), FileDirectoryInformation, NULL, FALSE); + } - status = NtOpenFile(&h, GENERIC_READ | SYNCHRONIZE, &obj_attr, &io_status, - FILE_SHARE_READ, FILE_SYNCHRONOUS_IO_NONALERT); + // Close the directory handle. + NtClose(Handle); - if(!NT_SUCCESS(status)) - HalReturnToFirmware(HalQuickRebootRoutine); + // Open the virtual device. + OBJECT_ATTRIBUTES DeviceObjectAttributes = { + .ObjectName = &VirtualDeviceName, + .Attributes = OBJ_CASE_INSENSITIVE, + }; - status = NtDeviceIoControlFile(h, NULL, NULL, NULL, &io_status, IOCTL_VIRTUAL_CDROM_DETACH, NULL, 0, NULL, 0); + Status = NtOpenFile(&Handle, GENERIC_READ | SYNCHRONIZE, &DeviceObjectAttributes, &IoStatusBlock, FILE_SHARE_READ, + FILE_SYNCHRONOUS_IO_NONALERT); - /* Note that opening the handle will also mount a file system. All access - * via the handle will go through the file system driver. Ideally we should - * first dismount any possibly mounted file system and then detach the virtual - * disc. It would be nice if we could open a direct access to the device driver, - * but this doesn't seem to be possible with the Xbox kernel API (opening with - * limited access rights doesn't yield the result.) - * - * We will just close the handle and dismount the automatically mounted file - * system. - */ - NtClose(h); - status = IoDismountVolumeByName(&dev_name); + if(!NT_SUCCESS(Status)) + goto CleanupAndExit; - status = NtOpenFile(&h, GENERIC_READ | SYNCHRONIZE, &obj_attr, &io_status, - FILE_SHARE_READ, - FILE_SYNCHRONOUS_IO_NONALERT); + Status = NtDeviceIoControlFile(Handle, NULL, NULL, NULL, &IoStatusBlock, IOCTL_VIRTUAL_CDROM_DETACH, NULL, 0, NULL, 0); - if(!NT_SUCCESS(status)) - HalReturnToFirmware(HalQuickRebootRoutine); + // Note that opening the handle will also mount a file system. All access via the handle will go through the file + // system driver. Ideally we should first dismount any possibly mounted file system and then detach the virtual + // disc. It would be nice if we could open a direct access to the device driver, but this doesn't seem to be possible + // with the Xbox kernel API (opening with limited access rights doesn't yield the result.) + // + // We will just close the handle and dismount the automatically mounted file system. + NtClose(Handle); + Status = IoDismountVolumeByName(&VirtualDeviceName); - status = NtDeviceIoControlFile(h, NULL, NULL, NULL, &io_status, - IOCTL_VIRTUAL_CDROM_ATTACH, - asd, sizeof(ATTACH_SLICE_DATA), NULL, 0); + Status = NtOpenFile(&Handle, GENERIC_READ | SYNCHRONIZE, &DeviceObjectAttributes, &IoStatusBlock, FILE_SHARE_READ, + FILE_SYNCHRONOUS_IO_NONALERT); - NtClose(h); + if(!NT_SUCCESS(Status)) { + NtClose(Handle); + goto CleanupAndExit; + } - /* Also note that access via our second file handle goes through the raw - * file system (there's no other choice since the virtual disc was detached). - * It's still mounted at this point. If we now want to access the attached - * image, we will first need to dismount that file system. - * - * It doesn't matter for this attach.xbe, but do it anyway so we won't forget - * about it in any future application. - */ - status = IoDismountVolumeByName(&dev_name); + Status = NtDeviceIoControlFile(Handle, NULL, NULL, NULL, &IoStatusBlock, IOCTL_VIRTUAL_CDROM_ATTACH, + (PVOID)&AttachSliceData, sizeof(AttachSliceData), NULL, 0); + NtClose(Handle); - HalReturnToFirmware(HalQuickRebootRoutine); + // Also note that access via our second file handle goes through the raw file system (there's no other choice since + // the virtual disc was detached). + // It's still mounted at this point. If we now want to access the attached image, we will first need to dismount + // that file system. + // It doesn't matter for this attach.xbe, but do it anyway so we won't forget about it in any future application. + Status = IoDismountVolumeByName(&VirtualDeviceName); +CleanupAndExit: return 0; } diff --git a/source/attach.h b/source/attach.h index 25dc6ed..4dd94e9 100644 --- a/source/attach.h +++ b/source/attach.h @@ -3,8 +3,8 @@ // SPDX-License-Identifier: GPL-2.0 // Based on virtualcdrom.h from driveimageutils by rmenhal #pragma once - #include +#include #include #define IOCTL_VIRTUAL_CDROM_ID 0x1EE7CD00 @@ -14,6 +14,11 @@ #define MAX_IMAGE_SLICES 8 typedef struct _ATTACH_SLICE_DATA { - uint32_t num_slices; - ANSI_STRING slice_files[MAX_IMAGE_SLICES]; + DWORD NumberOfSlices; + ANSI_STRING Files[MAX_IMAGE_SLICES]; } ATTACH_SLICE_DATA; + +typedef struct _FileInfo { + FILE_DIRECTORY_INFORMATION DirectoryInfo; + CHAR Filename[MAX_PATH]; +} FileInfo; diff --git a/source/helpers.c b/source/helpers.c new file mode 100644 index 0000000..e643755 --- /dev/null +++ b/source/helpers.c @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2004 rmenhal +// SPDX-FileCopyrightText: 2023 Dustin Holden +// SPDX-License-Identifier: GPL-2.0 +// Based on attach.c from driveimageutils by rmenhal +#include +#include +#include "helpers.h" + +// Helper strings +ANSI_STRING AllowedExtensions[] = { + RTL_CONSTANT_STRING(".iso"), + RTL_CONSTANT_STRING(".cso"), +}; + +// Helper function to check if a file has a specific extension. +BOOLEAN IsFileDiscImage(PANSI_STRING FileName) { + if(FileName->Length < 4) + return FALSE; + + for(DWORD Index = 0; Index < ARRAYSIZE(AllowedExtensions); Index++) { + PANSI_STRING Extension = &AllowedExtensions[Index]; + ANSI_STRING FileExtension = { + .Length = Extension->Length, + .MaximumLength = Extension->Length, + .Buffer = FileName->Buffer + FileName->Length - Extension->Length, + }; + + if(RtlEqualString(&FileExtension, Extension, TRUE)) + return TRUE; + } + + return FALSE; +} + +// Helper function to find the last occurrence of a character in a string. +// NOTE: This probably doesn't match the Win32 API implementation. +PCHAR StrRChr(PSTRING String, CHAR Char) { + for(PCHAR Ptr = String->Buffer + String->Length - 1; Ptr >= String->Buffer; Ptr--) { + if(*Ptr == Char) + return Ptr; + } + + return NULL; +} + +// Helper function to compare the tails of two strings. +LONG CompareStringTails(PANSI_STRING String1, PANSI_STRING String2, WORD Skip) { + ANSI_STRING n1, n2; + + if(String1->Length <= Skip) + return(String2->Length <= Skip) ? 0 : -1; + else if(String2->Length <= Skip) + return 1; + + n1.Length = String1->Length - Skip; + n1.MaximumLength = n1.Length; + n1.Buffer = String1->Buffer + Skip; + + n2.Length = String2->Length - Skip; + n2.MaximumLength = n2.Length; + n2.Buffer = String2->Buffer + Skip; + + return RtlCompareString(&n1, &n2, TRUE); +} diff --git a/source/helpers.h b/source/helpers.h new file mode 100644 index 0000000..eb48980 --- /dev/null +++ b/source/helpers.h @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2004 rmenhal +// SPDX-FileCopyrightText: 2023 Dustin Holden +// SPDX-License-Identifier: GPL-2.0 +// Based on virtualcdrom.h from driveimageutils by rmenhal +#pragma once +#include +#include + +#define ARRAYSIZE(a) (sizeof(a) / sizeof((a)[0])) + +// Functions +BOOLEAN IsFileDiscImage(PANSI_STRING FileName); +PCHAR StrRChr(PSTRING String, CHAR Char); +LONG CompareStringTails(PANSI_STRING String1, PANSI_STRING String2, WORD Skip);