; Disk driver to create FAT16/FAT32 memory-based temporary disk aka RAM disk.
; (c) CleverMouse

; Note: in the ideal world, a disk driver should not care about a file system
; on it. In the current world, however, there is no way to format a disk in
; FAT, so this part of file-system-specific operations is included in the
; driver.

; When this driver is loading, it registers itself in the system and does
; nothing more. When loaded, this driver controls pseudo-disk devices
; named /tmp#/, where # is a digit from 0 to 9. The driver does not create
; any device by itself, waiting for instructions from an application.
; The driver responds to the following IOCTLs from a control application:
SRV_GETVERSION          equ 0 ; input ignored,
                              ; output = dword API_VERSION
DEV_ADD_DISK            equ 1 ; input = structure add_disk_struc,
                              ; no output
DEV_DEL_DISK            equ 2 ; input = structure del_disk_struc,
                              ; no output
; For all IOCTLs the driver returns one of the following error codes:
NO_ERROR                equ 0
ERROR_INVALID_IOCTL     equ 1 ; unknown IOCTL code, wrong input/output size...
ERROR_INVALID_ID        equ 2 ; .DiskId must be from 0 to 9
ERROR_SIZE_TOO_LARGE    equ 3 ; .DiskSize is too large
ERROR_SIZE_TOO_SMALL    equ 4 ; .DiskSize is too small
ERROR_NO_MEMORY         equ 5 ; memory allocation failed

include '../struct.inc'

API_VERSION             equ 1
; Input structures:
struct add_disk_struc
DiskSize        dd      ? ; disk size in sectors, 1 sector = 512 bytes
; Note: DiskSize is the full size, including FAT service data.
; Size for useful data is slightly less than this number.
DiskId          db      ? ; from 0 to 9
ends
struct del_disk_struc
DiskId          db      ? ; from 0 to 9
ends

max_num_disks   equ     10

; standard driver stuff; version of driver model = 5
format PE DLL native 0.05

DEBUG equ 0

section '.flat' code readable writable executable
data fixups
end data
entry START
include '../proc32.inc'
include '../peimport.inc'
include '../macros.inc'
; the start procedure (see the description above)
proc START
; This procedure is called in two situations:
; when the driver is loading and when the system is shutting down.
; 1. Check that the driver is loading; do nothing unless so.
        xor     eax, eax ; set return value in case we will do nothing
        cmp     dword [esp+4], 1
        jne     .nothing
; 2. Register the driver in the system.
        invoke  RegService, my_service, service_proc
; 3. Return the value returned by RegService back to the system.
.nothing:
        retn
endp

; Service procedure for the driver - handle all IOCTL requests for the driver.
; The description of handled IOCTLs is located in the start of this file.
proc service_proc
; 1. Save used registers to be stdcall.
; Note: this shifts esp, so the first parameter [esp+4] becomes [esp+16].
; Note: edi is used not by this procedure itself, but by worker procedures.
        push    ebx esi edi
; 2. Get parameter from the stack: [esp+16] is the first parameter,
;    pointer to IOCTL structure.
        mov     edx, [esp+16]    ; edx -> IOCTL
; 3. Set the return value to 'invalid IOCTL'.
; Now, if one of conditions for IOCTL does not met, the code
; can simply return the value already loaded.
        mov     al, ERROR_INVALID_IOCTL
; 4. Get request code and select a handler for the code.
        mov     ecx, [edx+IOCTL.io_code]
        test    ecx, ecx        ; check for SRV_GETVERSION
        jnz     .no.srv_getversion
; 4. This is SRV_GETVERSION request, no input, 4 bytes output, API_VERSION.
; 4a. Output size must be at least 4 bytes.
        cmp     [edx+IOCTL.out_size], 4
        jl      .return
; 4b. Write result to the output buffer.
        mov     eax, [edx+IOCTL.output]
        mov     dword [eax], API_VERSION
; 4c. Return success.
        xor     eax, eax
        jmp     .return
.no.srv_getversion:
        dec     ecx     ; check for DEV_ADD_DISK
        jnz     .no.dev_add_disk
; 5. This is DEV_ADD_DISK request, input is add_disk_struc, output is 1 byte
; 5a. Input size must be exactly sizeof.add_disk_struc bytes.
        cmp     [edx+IOCTL.inp_size], sizeof.add_disk_struc
        jnz     .return
; 5b. Load input parameters and call the worker procedure.
        mov     eax, [edx+IOCTL.input]
        movzx   ebx, [eax+add_disk_struc.DiskId]
        mov     esi, [eax+add_disk_struc.DiskSize]
        call    add_disk
; 5c. Return back to the caller the value from the worker procedure.
        jmp     .return
.no.dev_add_disk:
        dec     ecx     ; check for DEV_DEL_DISK
        jnz     .return
; 6. This is DEV_DEL_DISK request, input is del_disk_struc
; 6a. Input size must be exactly sizeof.del_disk_struc bytes.
        cmp     [edx+IOCTL.inp_size], sizeof.del_disk_struc
        jnz     .return
; 6b. Load input parameters and call the worker procedure.
        mov     eax, [edx+IOCTL.input]
        movzx   ebx, [eax+del_disk_struc.DiskId]
        call    del_disk
; 6c. Return back to the caller the value from the worker procedure.
.return:
; 7. Exit.
; 7a. The code above returns a value in al for efficiency,
; propagate it to eax.
        movzx   eax, al
; 7b. Restore used registers to be stdcall.
        pop     edi esi ebx
; 7c. Return, popping one argument.
        retn    4
endp

; The worker procedure for DEV_ADD_DISK request.
; Creates a memory-based disk of given size and formats it in FAT16/32.
; Called with ebx = disk id, esi = disk size,
; returns error code in al.
proc add_disk
; 1. Check that disk id is correct and free.
; Otherwise, return the corresponding error code.
        mov     al, ERROR_INVALID_ID
        cmp     ebx, max_num_disks
        jae     .return
        cmp     [disk_pointers+ebx*4], 0
        jnz     .return
; 2. Check that the size is reasonable.
; Otherwise, return the corresponding error code.
        mov     al, ERROR_SIZE_TOO_LARGE
        cmp     esi, MAX_SIZE
        ja      .return
        mov     al, ERROR_SIZE_TOO_SMALL
        cmp     esi, MIN_FAT16_SIZE
        jb      .return
; 3. Allocate memory for the disk, store the pointer in edi.
; If failed, return the corresponding error code.
        mov     eax, esi
        shl     eax, 9
        invoke  KernelAlloc, eax
        mov     edi, eax
        test    eax, eax
        mov     al, ERROR_NO_MEMORY
        jz      .return
; 4. Store the pointer and the size in the global variables.
; It is possible, though very unlikely, that two threads
; have called this function in parallel with the same id,
; so [disk_pointers+ebx*4] could be filled by another thread.
; Play extra safe and store new value only if old value is zero.
        xor     eax, eax
        lock cmpxchg [disk_pointers+ebx*4], edi
        jz      @f
; Otherwise, free the allocated memory and return the corresponding error code.
        invoke  KernelFree, edi
        mov     al, ERROR_INVALID_ID
        jmp     .return
@@:
        mov     [disk_sizes+ebx*4], esi
; 5. Call the worker procedure for formatting this disk.
; It should not fail.
        call    format_disk
; 6. Register the disk in the system.
; 6a. Generate name as /tmp#, where # = ebx + '0'. Use two dwords in the stack.
        push    0
        push    'tmp'
        mov     eax, esp ; eax points to 'tmp' + zero byte + zero dword
        lea     ecx, [ebx+'0'] ; ecx = digit
        mov     [eax+3], cl ; eax points to 'tmp#' + zero dword
; 6b. Call the kernel API. Use disk id as 'userdata' parameter for callbacks.
        invoke  DiskAdd, disk_functions, eax, ebx, 0
; 6c. Restore the stack after 6a.
        pop     ecx ecx
; 6c. Check the result. If DiskAdd has failed, cleanup and return
; ERROR_NO_MEMORY, this is the most probable or even the only reason to fail.
        test    eax, eax
        jnz     @f
        mov     [disk_sizes+ebx*4], 0
        mov     [disk_pointers+ebx*4], 0
        invoke  KernelFree, edi
        mov     al, ERROR_NO_MEMORY
        jmp     .return
@@:
        push    eax
; 6d. Notify the kernel that media is inserted.
        invoke  DiskMediaChanged, eax, 1
; 6e. Disk is fully configured; store its handle in the global variable
; and return success.
        pop     [disk_handles+ebx*4]
        xor     eax, eax
; 7. Return.
.return:
        retn
endp

; The worker procedure for DEV_DEL_DISK request.
; Deletes a previously created memory-based disk.
; Called with ebx = disk id,
; returns error code in al.
proc del_disk
; 1. Check that disk id is correct.
; Otherwise, return the corresponding error code.
        mov     al, ERROR_INVALID_ID
        cmp     ebx, max_num_disks
        jae     .return
; 2. Get the disk handle, simultaneously clearing the global variable.
        xor     edx, edx
        xchg    edx, [disk_handles+ebx*4]
; 3. Check that the handle is non-zero.
; Otherwise, return the corresponding error code.
        test    edx, edx
        jz      .return
; 4. Delete the disk from the system.
        invoke  DiskDel, edx
; 5. Return success.
; Note that we can't free memory yet; it will be done in tmpdisk_close.
        xor     eax, eax
.return:
        retn
endp

; Include implementation of tmpdisk_* callbacks.
include 'tmpdisk_work.inc'
; Include FAT-specific code.
include 'tmpdisk_fat.inc'

; initialized data
align 4
disk_functions:
        dd      disk_functions_end - disk_functions
        dd      tmpdisk_close
        dd      0 ; no need in .closemedia
        dd      tmpdisk_querymedia
        dd      tmpdisk_read
        dd      tmpdisk_write
        dd      0 ; no need in .flush
        dd      tmpdisk_adjust_cache_size
disk_functions_end:
; disk_handles = array of values for Disk* kernel functions
label disk_handles dword
times max_num_disks dd 0
; disk_pointers = array of pointers to disk data
label disk_pointers dword
times max_num_disks dd 0
; disk_sizes = array of disk sizes
label disk_sizes dword
times max_num_disks dd 0

my_service      db      'tmpdisk',0