; -*- fundamental -*- (asm-mode sucks)
; ****************************************************************************
;
;  pxelinux.asm
;
;  A program to boot Linux kernels off a TFTP server using the Intel PXE
;  network booting API.  It is based on the SYSLINUX boot loader for
;  MS-DOS floppies.
;
;   Copyright (C) 1994-2007  H. Peter Anvin
;
;  This program is free software; you can redistribute it and/or modify
;  it under the terms of the GNU General Public License as published by
;  the Free Software Foundation, Inc., 53 Temple Place Ste 330,
;  Boston MA 02111-1307, USA; either version 2 of the License, or
;  (at your option) any later version; incorporated herein by reference.
;
; ****************************************************************************

%define IS_PXELINUX 1
%include "head.inc"
%include "pxe.inc"

;
; Some semi-configurable constants... change on your own risk.
;
my_id		equ pxelinux_id
FILENAME_MAX_LG2 equ 7			; log2(Max filename size Including final null)
FILENAME_MAX	equ (1 << FILENAME_MAX_LG2)
NULLFILE	equ 0			; Zero byte == null file name
NULLOFFSET	equ 4			; Position in which to look
REBOOT_TIME	equ 5*60		; If failure, time until full reset
%assign HIGHMEM_SLOP 128*1024		; Avoid this much memory near the top
MAX_OPEN_LG2	equ 5			; log2(Max number of open sockets)
MAX_OPEN	equ (1 << MAX_OPEN_LG2)
PKTBUF_SIZE	equ (65536/MAX_OPEN)	; Per-socket packet buffer size
TFTP_PORT	equ htons(69)		; Default TFTP port
PKT_RETRY	equ 6			; Packet transmit retry count
PKT_TIMEOUT	equ 12			; Initial timeout, timer ticks @ 55 ms
; Desired TFTP block size
; For Ethernet MTU is normally 1500.  Unfortunately there seems to
; be a fair number of networks with "substandard" MTUs which break.
; The code assumes TFTP_LARGEBLK <= 2K.
TFTP_MTU	equ 1440
TFTP_LARGEBLK	equ (TFTP_MTU-20-8-4)	; MTU - IP hdr - UDP hdr - TFTP hdr
; Standard TFTP block size
TFTP_BLOCKSIZE_LG2 equ 9		; log2(bytes/block)
TFTP_BLOCKSIZE	equ (1 << TFTP_BLOCKSIZE_LG2)
%assign USE_PXE_PROVIDED_STACK 1	; Use stack provided by PXE?

SECTOR_SHIFT	equ TFTP_BLOCKSIZE_LG2
SECTOR_SIZE	equ TFTP_BLOCKSIZE

;
; This is what we need to do when idle
; *** This is disabled because some PXE stacks wait for unacceptably
; *** long if there are no packets receivable.

%define HAVE_IDLE 0			; idle is not a noop

%if HAVE_IDLE
%macro	RESET_IDLE 0
	call reset_idle
%endmacro
%macro	DO_IDLE 0
	call check_for_arp
%endmacro
%else
%macro	RESET_IDLE 0
	; Nothing
%endmacro
%macro	DO_IDLE 0
	; Nothing
%endmacro
%endif

;
; TFTP operation codes
;
TFTP_RRQ	equ htons(1)		; Read request
TFTP_WRQ	equ htons(2)		; Write request
TFTP_DATA	equ htons(3)		; Data packet
TFTP_ACK	equ htons(4)		; ACK packet
TFTP_ERROR	equ htons(5)		; ERROR packet
TFTP_OACK	equ htons(6)		; OACK packet

;
; TFTP error codes
;
TFTP_EUNDEF	equ htons(0)		; Unspecified error
TFTP_ENOTFOUND	equ htons(1)		; File not found
TFTP_EACCESS	equ htons(2)		; Access violation
TFTP_ENOSPACE	equ htons(3)		; Disk full
TFTP_EBADOP	equ htons(4)		; Invalid TFTP operation
TFTP_EBADID	equ htons(5)		; Unknown transfer
TFTP_EEXISTS	equ htons(6)		; File exists
TFTP_ENOUSER	equ htons(7)		; No such user
TFTP_EOPTNEG	equ htons(8)		; Option negotiation failure

;
; The following structure is used for "virtual kernels"; i.e. LILO-style
; option labels.  The options we permit here are `kernel' and `append
; Since there is no room in the bottom 64K for all of these, we
; stick them at vk_seg:0000 and copy them down before we need them.
;
		struc vkernel
vk_vname:	resb FILENAME_MAX	; Virtual name **MUST BE FIRST!**
vk_rname:	resb FILENAME_MAX	; Real name
vk_ipappend:	resb 1			; "IPAPPEND" flag
vk_type:	resb 1			; Type of file
vk_appendlen:	resw 1
		alignb 4
vk_append:	resb max_cmd_len+1	; Command line
		alignb 4
vk_end:		equ $			; Should be <= vk_size
		endstruc

;
; Segment assignments in the bottom 640K
; 0000h - main code/data segment (and BIOS segment)
;
real_mode_seg	equ 4000h
pktbuf_seg	equ 3000h		; Packet buffers segments
vk_seg          equ 2000h		; Virtual kernels
xfer_buf_seg	equ 1000h		; Bounce buffer for I/O to high mem
comboot_seg	equ real_mode_seg	; COMBOOT image loading zone

;
; BOOTP/DHCP packet pattern
;
		struc bootp_t
bootp:
.opcode		resb 1			; BOOTP/DHCP "opcode"
.hardware	resb 1			; ARP hardware type
.hardlen	resb 1			; Hardware address length
.gatehops	resb 1			; Used by forwarders
.ident		resd 1			; Transaction ID
.seconds	resw 1			; Seconds elapsed
.flags		resw 1			; Broadcast flags
.cip		resd 1			; Client IP
.yip		resd 1			; "Your" IP
.sip		resd 1			; Next server IP
.gip		resd 1			; Relay agent IP
.macaddr	resb 16			; Client MAC address
.sname		resb 64			; Server name (optional)
.bootfile	resb 128		; Boot file name
.option_magic	resd 1			; Vendor option magic cookie
.options	resb 1260		; Vendor options
		endstruc

BOOTP_OPTION_MAGIC	equ htonl(0x63825363)	; See RFC 2132

;
; TFTP connection data structure.  Each one of these corresponds to a local
; UDP port.  The size of this structure must be a power of 2.
; HBO = host byte order; NBO = network byte order
; (*) = written by options negotiation code, must be dword sized
;
		struc open_file_t
tftp_localport	resw 1			; Local port number	(0 = not in use)
tftp_remoteport	resw 1			; Remote port number
tftp_remoteip	resd 1			; Remote IP address
tftp_filepos	resd 1			; Bytes downloaded (including buffer)
tftp_filesize	resd 1			; Total file size(*)
tftp_blksize	resd 1			; Block size for this connection(*)
tftp_bytesleft	resw 1			; Unclaimed data bytes
tftp_lastpkt	resw 1			; Sequence number of last packet (NBO)
tftp_dataptr	resw 1			; Pointer to available data
		resw 2			; Currently unusued
		; At end since it should not be zeroed on socked close
tftp_pktbuf	resw 1			; Packet buffer offset
		endstruc
%ifndef DEPEND
%if (open_file_t_size & (open_file_t_size-1))
%error "open_file_t is not a power of 2"
%endif
%endif

; ---------------------------------------------------------------------------
;   BEGIN CODE
; ---------------------------------------------------------------------------

;
; Memory below this point is reserved for the BIOS and the MBR
;
		section .earlybss
trackbufsize	equ 8192
trackbuf	resb trackbufsize	; Track buffer goes here
getcbuf		resb trackbufsize
		; ends at 4800h

		; Put some large buffers here, before RBFG_brainfuck,
		; where we can still carefully control the address
		; assignments...

		alignb open_file_t_size
Files		resb MAX_OPEN*open_file_t_size

		alignb FILENAME_MAX
BootFile	resb 256		; Boot file from DHCP packet
PathPrefix	resb 256		; Path prefix derived from boot file
DotQuadBuf	resb 16			; Buffer for dotted-quad IP address
IPOption	resb 80			; ip= option buffer
InitStack	resd 1			; Pointer to reset stack (SS:SP)
PXEStack	resd 1			; Saved stack during PXE call

; Warning here: RBFG build 22 randomly overwrites memory location
; [0x5680,0x576c), possibly more.  It seems that it gets confused and
; screws up the pointer to its own internal packet buffer and starts
; writing a received ARP packet into low memory.
RBFG_brainfuck	resb 0E00h

		section .latebss
		alignb 4
RebootTime	resd 1			; Reboot timeout, if set by option
StrucPtr	resd 1			; Pointer to PXENV+ or !PXE structure
APIVer		resw 1			; PXE API version found
IPOptionLen	resw 1			; Length of IPOption
IdleTimer	resw 1			; Time to check for ARP?
LocalBootType	resw 1			; Local boot return code
PktTimeout	resw 1			; Timeout for current packet
RealBaseMem	resw 1			; Amount of DOS memory after freeing
OverLoad	resb 1			; Set if DHCP packet uses "overloading"
DHCPMagic	resb 1			; PXELINUX magic flags

; The relative position of these fields matter!
MAC_MAX		equ  32			; Handle hardware addresses this long
MACLen		resb 1			; MAC address len
MACType		resb 1			; MAC address type
MAC		resb MAC_MAX+1		; Actual MAC address
BOOTIFStr	resb 7			; Space for "BOOTIF="
MACStr		resb 3*(MAC_MAX+1)	; MAC address as a string

; The relative position of these fields matter!
UUIDType	resb 1			; Type byte from DHCP option
UUID		resb 16			; UUID, from the PXE stack
UUIDNull	resb 1			; dhcp_copyoption zero-terminates

;
; PXE packets which don't need static initialization
;
		alignb 4
pxe_unload_stack_pkt:
.status:	resw 1			; Status
.reserved:	resw 10			; Reserved
pxe_unload_stack_pkt_len	equ $-pxe_unload_stack_pkt

		alignb 16
		; BOOTP/DHCP packet buffer

		alignb 16
packet_buf	resb 2048		; Transfer packet
packet_buf_size	equ $-packet_buf

;
; Constants for the xfer_buf_seg
;
; The xfer_buf_seg is also used to store message file buffers.  We
; need two trackbuffers (text and graphics), plus a work buffer
; for the graphics decompressor.
;
xbs_textbuf	equ 0			; Also hard-coded, do not change
xbs_vgabuf	equ trackbufsize
xbs_vgatmpbuf	equ 2*trackbufsize

		section .text
		;
		; PXELINUX needs more BSS than the other derivatives;
		; therefore we relocate it from 7C00h on startup.
		;
StackBuf	equ $			; Base of stack if we use our own

;
; Primary entry point.
;
bootsec		equ $
_start:
		pushfd			; Paranoia... in case of return to PXE
		pushad			; ... save as much state as possible
		push ds
		push es
		push fs
		push gs

		xor ax,ax
		mov ds,ax
		mov es,ax

%ifndef DEPEND
%if TEXT_START != 0x7c00
		; This is uglier than it should be, but works around
		; some NASM 0.98.38 bugs.
		mov di,section..bcopy32.start
		add di,__bcopy_size-4
		lea si,[di-(TEXT_START-7C00h)]
		lea cx,[di-(TEXT_START-4)]
		shr cx,2
		std			; Overlapping areas, copy backwards
		rep movsd
%endif
%endif
		jmp 0:_start1		; Canonicalize address
_start1:
		mov bp,sp
		les bx,[bp+48]		; ES:BX -> !PXE or PXENV+ structure

		; That is all pushed onto the PXE stack.  Save the pointer
		; to it and switch to an internal stack.
		mov [InitStack],sp
		mov [InitStack+2],ss

%if USE_PXE_PROVIDED_STACK
		; Apparently some platforms go bonkers if we
		; set up our own stack...
		mov [BaseStack],sp
		mov [BaseStack+4],ss
%endif

		cli			; Paranoia
		lss esp,[BaseStack]

		sti			; Stack set up and ready
		cld			; Copy upwards

;
; Initialize screen (if we're using one)
;
		push es			; Save ES -> PXE entry structure
		push ds
		pop es			; ES <- DS
%include "init.inc"
		pop es			; Restore ES -> PXE entry structure
;
; Tell the user we got this far
;
		mov si,syslinux_banner
		call writestr

		mov si,copyright_str
		call writestr

;
; Assume API version 2.1, in case we find the !PXE structure without
; finding the PXENV+ structure.  This should really look at the Base
; Code ROM ID structure in have_pxe, but this is adequate for now --
; if we have !PXE, we have to be 2.1 or higher, and we don't care
; about higher versions than that.
;
		mov word [APIVer],0201h

;
; Now we need to find the !PXE structure.  It's *supposed* to be pointed
; to by SS:[SP+4], but support INT 1Ah, AX=5650h method as well.
; FIX: ES:BX should point to the PXENV+ structure on entry as well.
; We should make that the second test, and not trash ES:BX...
;
		cmp dword [es:bx], '!PXE'
		je have_pxe

		; Uh-oh, not there... try plan B
		mov ax, 5650h
%if USE_PXE_PROVIDED_STACK == 0
		lss sp,[InitStack]
%endif
		int 1Ah					; May trash regs
%if USE_PXE_PROVIDED_STACK == 0
		lss esp,[BaseStack]
%endif

		jc no_pxe
		cmp ax,564Eh
		jne no_pxe

		; Okay, that gave us the PXENV+ structure, find !PXE
		; structure from that (if available)
		cmp dword [es:bx], 'PXEN'
		jne no_pxe
		cmp word [es:bx+4], 'V+'
		je have_pxenv

		; Nothing there either.  Last-ditch: scan memory
		call memory_scan_for_pxe_struct		; !PXE scan
		jnc have_pxe
		call memory_scan_for_pxenv_struct	; PXENV+ scan
		jnc have_pxenv

no_pxe:		mov si,err_nopxe
		call writestr
		jmp kaboom

have_pxenv:
		mov [StrucPtr],bx
		mov [StrucPtr+2],es

		mov si,found_pxenv
		call writestr

		mov si,apiver_str
		call writestr
		mov ax,[es:bx+6]
		mov [APIVer],ax
		call writehex4
		call crlf

		cmp ax,0201h			; API version 2.1 or higher
		jb old_api
		mov si,bx
		mov ax,es
		les bx,[es:bx+28h]		; !PXE structure pointer
		cmp dword [es:bx],'!PXE'
		je have_pxe

		; Nope, !PXE structure missing despite API 2.1+, or at least
		; the pointer is missing.  Do a last-ditch attempt to find it.
		call memory_scan_for_pxe_struct
		jnc have_pxe

		; Otherwise, no dice, use PXENV+ structure
		mov bx,si
		mov es,ax

old_api:	; Need to use a PXENV+ structure
		mov si,using_pxenv_msg
		call writestr

		mov eax,[es:bx+0Ah]		; PXE RM API
		mov [PXENVEntry],eax

		mov si,undi_data_msg
		call writestr
		mov ax,[es:bx+20h]
		call writehex4
		call crlf
		mov si,undi_data_len_msg
		call writestr
		mov ax,[es:bx+22h]
		call writehex4
		call crlf
		mov si,undi_code_msg
		call writestr
		mov ax,[es:bx+24h]
		call writehex4
		call crlf
		mov si,undi_code_len_msg
		call writestr
		mov ax,[es:bx+26h]
		call writehex4
		call crlf

		; Compute base memory size from PXENV+ structure
		xor esi,esi
		movzx eax,word [es:bx+20h]	; UNDI data seg
		cmp ax,[es:bx+24h]		; UNDI code seg
		ja .use_data
		mov ax,[es:bx+24h]
		mov si,[es:bx+26h]
		jmp short .combine
.use_data:
		mov si,[es:bx+22h]
.combine:
		shl eax,4
		add eax,esi
		shr eax,10			; Convert to kilobytes
		mov [RealBaseMem],ax

		mov si,pxenventry_msg
		call writestr
		mov ax,[PXENVEntry+2]
		call writehex4
		mov al,':'
		call writechr
		mov ax,[PXENVEntry]
		call writehex4
		call crlf
		jmp have_entrypoint

have_pxe:
		mov [StrucPtr],bx
		mov [StrucPtr+2],es

		mov eax,[es:bx+10h]
		mov [PXEEntry],eax

		mov si,undi_data_msg
		call writestr
		mov eax,[es:bx+2Ah]
		call writehex8
		call crlf
		mov si,undi_data_len_msg
		call writestr
		mov ax,[es:bx+2Eh]
		call writehex4
		call crlf
		mov si,undi_code_msg
		call writestr
		mov ax,[es:bx+32h]
		call writehex8
		call crlf
		mov si,undi_code_len_msg
		call writestr
		mov ax,[es:bx+36h]
		call writehex4
		call crlf

		; Compute base memory size from !PXE structure
		xor esi,esi
		mov eax,[es:bx+2Ah]
		cmp eax,[es:bx+32h]
		ja .use_data
		mov eax,[es:bx+32h]
		mov si,[es:bx+36h]
		jmp short .combine
.use_data:
		mov si,[es:bx+2Eh]
.combine:
		add eax,esi
		shr eax,10
		mov [RealBaseMem],ax

		mov si,pxeentry_msg
		call writestr
		mov ax,[PXEEntry+2]
		call writehex4
		mov al,':'
		call writechr
		mov ax,[PXEEntry]
		call writehex4
		call crlf

have_entrypoint:
		push cs
		pop es				; Restore CS == DS == ES

;
; Network-specific initialization
;
		xor ax,ax
		mov [LocalDomain],al		; No LocalDomain received

;
; The DHCP client identifiers are best gotten from the DHCPREQUEST
; packet (query info 1).
;
query_bootp_1:
		mov dl,1
		call pxe_get_cached_info
		call parse_dhcp

		; We don't use flags from the request packet, so
		; this is a good time to initialize DHCPMagic...
		mov byte [DHCPMagic],0

;
; Now attempt to get the BOOTP/DHCP packet that brought us life (and an IP
; address).  This lives in the DHCPACK packet (query info 2).
;
query_bootp_2:
		mov dl,2
		call pxe_get_cached_info
		call parse_dhcp			; Parse DHCP packet
;
; Save away MAC address (assume this is in query info 2.  If this
; turns out to be problematic it might be better getting it from
; the query info 1 packet.)
;
.save_mac:
		movzx cx,byte [trackbuf+bootp.hardlen]
		cmp cx,16
		jna .mac_ok
		xor cx,cx		; Bad hardware address length
.mac_ok:
		mov [MACLen],cl
		mov al,[trackbuf+bootp.hardware]
		mov [MACType],al
		mov si,trackbuf+bootp.macaddr
		mov di,MAC
		rep movsb

; Enable this if we really need to zero-pad this field...
;		mov cx,MAC+MAC_MAX+1
;		sub cx,di
;		xor ax,ax
;		rep stosb

;
; Now, get the boot file and other info.  This lives in the CACHED_REPLY
; packet (query info 3).
;
		mov dl,3
		call pxe_get_cached_info
		call parse_dhcp			; Parse DHCP packet

;
; Generate the bootif string, and the hardware-based config string.
;
make_bootif_string:
		mov si,bootif_str
		mov di,BOOTIFStr
		mov cx,bootif_str_len
		rep movsb

		movzx cx,byte [MACLen]
		mov si,MACType
		inc cx
.hexify_mac:
		push cx
		mov cl,1		; CH == 0 already
		call lchexbytes
		mov al,'-'
		stosb
		pop cx
		loop .hexify_mac
		mov [di-1],cl		; Null-terminate and strip final dash
;
; Generate ip= option
;
		call genipopt

;
; Print IP address
;
		mov eax,[MyIP]
		mov di,DotQuadBuf
		push di
		call gendotquad			; This takes network byte order input

		xchg ah,al			; Convert to host byte order
		ror eax,16			; (BSWAP doesn't work on 386)
		xchg ah,al

		mov si,myipaddr_msg
		call writestr
		call writehex8
		mov al,' '
		call writechr
		pop si				; DotQuadBuf
		call writestr
		call crlf

		mov si,IPOption
		call writestr
		call crlf

;
; Check to see if we got any PXELINUX-specific DHCP options; in particular,
; if we didn't get the magic enable, do not recognize any other options.
;
check_dhcp_magic:
		test byte [DHCPMagic], 1	; If we didn't get the magic enable...
		jnz .got_magic
		mov byte [DHCPMagic], 0		; If not, kill all other options
.got_magic:


;
; Initialize UDP stack
;
udp_init:
		mov eax,[MyIP]
		mov [pxe_udp_open_pkt.sip],eax
		mov di,pxe_udp_open_pkt
		mov bx,PXENV_UDP_OPEN
		call pxenv
		jc .failed
		cmp word [pxe_udp_open_pkt.status], byte 0
		je .success
.failed:	mov si,err_udpinit
		call writestr
		jmp kaboom
.success:

;
; Common initialization code
;
%include "cpuinit.inc"

;
; Now we're all set to start with our *real* business.	First load the
; configuration file (if any) and parse it.
;
; In previous versions I avoided using 32-bit registers because of a
; rumour some BIOSes clobbered the upper half of 32-bit registers at
; random.  I figure, though, that if there are any of those still left
; they probably won't be trying to install Linux on them...
;
; The code is still ripe with 16-bitisms, though.  Not worth the hassle
; to take'm out.  In fact, we may want to put them back if we're going
; to boot ELKS at some point.
;

;
; Store standard filename prefix
;
prefix:		test byte [DHCPMagic], 04h	; Did we get a path prefix option
		jnz .got_prefix
		mov si,BootFile
		mov di,PathPrefix
		cld
		call strcpy
		mov cx,di
		sub cx,PathPrefix+1
		std
		lea si,[di-2]			; Skip final null!
.find_alnum:	lodsb
		or al,20h
		cmp al,'.'			; Count . or - as alphanum
		je .alnum
		cmp al,'-'
		je .alnum
		cmp al,'0'
		jb .notalnum
		cmp al,'9'
		jbe .alnum
		cmp al,'a'
		jb .notalnum
		cmp al,'z'
		ja .notalnum
.alnum:		loop .find_alnum
		dec si
.notalnum:	mov byte [si+2],0		; Zero-terminate after delimiter
		cld
.got_prefix:
		mov si,tftpprefix_msg
		call writestr
		mov si,PathPrefix
		call writestr
		call crlf

;
; Load configuration file
;
find_config:

;
; Begin looking for configuration file
;
config_scan:
		test byte [DHCPMagic], 02h
		jz .no_option

		; We got a DHCP option, try it first
		call .try
		jnz .success

.no_option:
		mov di,ConfigName
		mov si,cfgprefix
		mov cx,cfgprefix_len
		rep movsb

		; Have to guess config file name...

		; Try loading by UUID.
		cmp byte [HaveUUID],0
		je .no_uuid

		push di
		mov bx,uuid_dashes
		mov si,UUID
.gen_uuid:
		movzx cx,byte [bx]
		jcxz .done_uuid
		inc bx
		call lchexbytes
		mov al,'-'
		stosb
		jmp .gen_uuid
.done_uuid:
		mov [di-1],cl		; Remove last dash and zero-terminate
		pop di
		call .try
		jnz .success
.no_uuid:

		; Try loading by MAC address
		push di
		mov si,MACStr
		call strcpy
		pop di
		call .try
		jnz .success

		; Nope, try hexadecimal IP prefixes...
.scan_ip:
		mov cx,4
		mov si,MyIP
		call uchexbytes			; Convert to hex string

		mov cx,8			; Up to 8 attempts
.tryagain:
		mov byte [di],0			; Zero-terminate string
		call .try
		jnz .success
		dec di				; Drop one character
		loop .tryagain

		; Final attempt: "default" string
		mov si,default_str		; "default" string
		call strcpy
		call .try
		jnz .success

		mov si,err_noconfig
		call writestr
		jmp kaboom

.try:
		pusha
		mov si,trying_msg
		call writestr
		mov di,ConfigName
		mov si,di
		call writestr
		call crlf
		mov si,di
		mov di,getcbuf
		call mangle_name
		call open
		popa
		ret


.success:

;
; Linux kernel loading code is common.  However, we need to define
; a couple of helper macros...
;

; Handle "ipappend" option
%define HAVE_SPECIAL_APPEND
%macro	SPECIAL_APPEND 0
		test byte [IPAppend],01h	; ip=
		jz .noipappend1
		mov si,IPOption
		mov cx,[IPOptionLen]
		rep movsb
		mov al,' '
		stosb
.noipappend1:
		test byte [IPAppend],02h
		jz .noipappend2
		mov si,BOOTIFStr
		call strcpy
		mov byte [es:di-1],' '		; Replace null with space
.noipappend2:
%endmacro

; Unload PXE stack
%define HAVE_UNLOAD_PREP
%macro	UNLOAD_PREP 0
		call unload_pxe
%endmacro

;
; Now we have the config file open.  Parse the config file and
; run the user interface.
;
%include "ui.inc"

;
; Boot to the local disk by returning the appropriate PXE magic.
; AX contains the appropriate return code.
;
local_boot:
		push cs
		pop ds
		mov [LocalBootType],ax
		call vgaclearmode
		mov si,localboot_msg
		call writestr
		; Restore the environment we were called with
		lss sp,[InitStack]
		pop gs
		pop fs
		pop es
		pop ds
		popad
		mov ax,[cs:LocalBootType]
		popfd
		retf				; Return to PXE

;
; kaboom: write a message and bail out.  Wait for quite a while,
;	  or a user keypress, then do a hard reboot.
;
kaboom:
		RESET_STACK_AND_SEGS AX
.patch:		mov si,bailmsg
		call writestr		; Returns with AL = 0
.drain:		call pollchar
		jz .drained
		call getchar
		jmp short .drain
.drained:
		mov edi,[RebootTime]
		mov al,[DHCPMagic]
		and al,09h		; Magic+Timeout
		cmp al,09h
		je .time_set
		mov edi,REBOOT_TIME
.time_set:
		mov cx,18
.wait1:		push cx
		mov ecx,edi
.wait2:		mov dx,[BIOS_timer]
.wait3:		call pollchar
		jnz .keypress
		cmp dx,[BIOS_timer]
		je .wait3
		loop .wait2,ecx
		mov al,'.'
		call writechr
		pop cx
		loop .wait1
.keypress:
		call crlf
		mov word [BIOS_magic],0	; Cold reboot
		jmp 0F000h:0FFF0h	; Reset vector address

;
; memory_scan_for_pxe_struct:
;
;	If none of the standard methods find the !PXE structure, look for it
;	by scanning memory.
;
;	On exit, if found:
;		CF = 0, ES:BX -> !PXE structure
;	Otherwise CF = 1, all registers saved
;
memory_scan_for_pxe_struct:
		push ds
		pusha
		mov ax,cs
		mov ds,ax
		mov si,trymempxe_msg
		call writestr
		mov ax,[BIOS_fbm]	; Starting segment
		shl ax,(10-4)		; Kilobytes -> paragraphs
;		mov ax,01000h		; Start to look here
		dec ax			; To skip inc ax
.mismatch:
		inc ax
		cmp ax,0A000h		; End of memory
		jae .not_found
		call writehex4
		mov si,fourbs_msg
		call writestr
		mov es,ax
		mov edx,[es:0]
		cmp edx,'!PXE'
		jne .mismatch
		movzx cx,byte [es:4]	; Length of structure
		cmp cl,08h		; Minimum length
		jb .mismatch
		push ax
		xor ax,ax
		xor si,si
.checksum:	es lodsb
		add ah,al
		loop .checksum
		pop ax
		jnz .mismatch		; Checksum must == 0
.found:		mov bp,sp
		xor bx,bx
		mov [bp+8],bx		; Save BX into stack frame (will be == 0)
		mov ax,es
		call writehex4
		call crlf
		popa
		pop ds
		clc
		ret
.not_found:	mov si,notfound_msg
		call writestr
		popa
		pop ds
		stc
		ret

;
; memory_scan_for_pxenv_struct:
;
;	If none of the standard methods find the PXENV+ structure, look for it
;	by scanning memory.
;
;	On exit, if found:
;		CF = 0, ES:BX -> PXENV+ structure
;	Otherwise CF = 1, all registers saved
;
memory_scan_for_pxenv_struct:
		pusha
		mov si,trymempxenv_msg
		call writestr
;		mov ax,[BIOS_fbm]	; Starting segment
;		shl ax,(10-4)		; Kilobytes -> paragraphs
		mov ax,01000h		; Start to look here
		dec ax			; To skip inc ax
.mismatch:
		inc ax
		cmp ax,0A000h		; End of memory
		jae .not_found
		mov es,ax
		mov edx,[es:0]
		cmp edx,'PXEN'
		jne .mismatch
		mov dx,[es:4]
		cmp dx,'V+'
		jne .mismatch
		movzx cx,byte [es:8]	; Length of structure
		cmp cl,26h		; Minimum length
		jb .mismatch
		xor ax,ax
		xor si,si
.checksum:	es lodsb
		add ah,al
		loop .checksum
		and ah,ah
		jnz .mismatch		; Checksum must == 0
.found:		mov bp,sp
		mov [bp+8],bx		; Save BX into stack frame
		mov ax,bx
		call writehex4
		call crlf
		clc
		ret
.not_found:	mov si,notfound_msg
		call writestr
		popad
		stc
		ret

;
; close_file:
;	     Deallocates a file structure (pointer in SI)
;	     Assumes CS == DS.
;
; XXX: We should check to see if this file is still open on the server
; side and send a courtesy ERROR packet to the server.
;
close_file:
		and si,si
		jz .closed
		mov word [si],0		; Not in use
.closed:	ret

;
; searchdir:
;
;	Open a TFTP connection to the server
;
;	     On entry:
;		DS:DI	= mangled filename
;	     If successful:
;		ZF clear
;		SI	= socket pointer
;		DX:AX	= file length in bytes
;	     If unsuccessful
;		ZF set
;

searchdir:
		push es
		push bx
		push cx
		mov ax,ds
		mov es,ax
		mov si,di
		push bp
		mov bp,sp

		call allocate_socket
		jz .ret

		mov ax,PKT_RETRY	; Retry counter
		mov word [PktTimeout],PKT_TIMEOUT	; Initial timeout

.sendreq:	push ax			; [bp-2]  - Retry counter
		push si			; [bp-4]  - File name

		mov di,packet_buf
		mov [pxe_udp_write_pkt.buffer],di

		mov ax,TFTP_RRQ		; TFTP opcode
		stosw

		lodsd			; EAX <- server override (if any)
		and eax,eax
		jnz .noprefix		; No prefix, and we have the server

		push si			; Add common prefix
		mov si,PathPrefix
		call strcpy
		dec di
		pop si

		mov eax,[ServerIP]	; Get default server

.noprefix:
		call strcpy		; Filename

		mov [bx+tftp_remoteip],eax

		push bx			; [bp-6]  - TFTP block
		mov bx,[bx]
		push bx			; [bp-8]  - TID (local port no)

		mov [pxe_udp_write_pkt.status],byte 0
		mov [pxe_udp_write_pkt.sip],eax
		; Now figure out the gateway
		xor eax,[MyIP]
		and eax,[Netmask]
		jz .nogwneeded
		mov eax,[Gateway]
.nogwneeded:
		mov [pxe_udp_write_pkt.gip],eax
		mov [pxe_udp_write_pkt.lport],bx
		mov ax,[ServerPort]
		mov [pxe_udp_write_pkt.rport],ax
		mov si,tftp_tail
		mov cx,tftp_tail_len
		rep movsb
		sub di,packet_buf	; Get packet size
		mov [pxe_udp_write_pkt.buffersize],di

		mov di,pxe_udp_write_pkt
		mov bx,PXENV_UDP_WRITE
		call pxenv
		jc .failure
		cmp word [pxe_udp_write_pkt.status],byte 0
		jne .failure

		;
		; Danger, Will Robinson!  We need to support timeout
		; and retry lest we just lost a packet...
		;

		; Packet transmitted OK, now we need to receive
.getpacket:	push word [PktTimeout]	; [bp-10]
		push word [BIOS_timer]	; [bp-12]

.pkt_loop:	mov bx,[bp-8]		; TID
		mov di,packet_buf
		mov word [pxe_udp_read_pkt.status],0
		mov [pxe_udp_read_pkt.buffer],di
		mov [pxe_udp_read_pkt.buffer+2],ds
		mov word [pxe_udp_read_pkt.buffersize],packet_buf_size
		mov eax,[MyIP]
		mov [pxe_udp_read_pkt.dip],eax
		mov [pxe_udp_read_pkt.lport],bx
		mov di,pxe_udp_read_pkt
		mov bx,PXENV_UDP_READ
		call pxenv
		and ax,ax
		jz .got_packet			; Wait for packet
.no_packet:
		mov dx,[BIOS_timer]
		cmp dx,[bp-12]
		je .pkt_loop
		mov [bp-12],dx
		dec word [bp-10]		; Timeout
		jnz .pkt_loop
		pop ax	; Adjust stack
		pop ax
		shl word [PktTimeout],1		; Exponential backoff
		jmp .failure

.got_packet:
		mov si,[bp-6]			; TFTP pointer
		mov bx,[bp-8]			; TID

		mov eax,[si+tftp_remoteip]
		cmp [pxe_udp_read_pkt.sip],eax	; This is technically not to the TFTP spec?
		jne .no_packet

		; Got packet - reset timeout
		mov word [PktTimeout],PKT_TIMEOUT

		pop ax	; Adjust stack
		pop ax

		mov ax,[pxe_udp_read_pkt.rport]
		mov [si+tftp_remoteport],ax

		; filesize <- -1 == unknown
		mov dword [si+tftp_filesize], -1
		; Default blksize unless blksize option negotiated
		mov word [si+tftp_blksize], TFTP_BLOCKSIZE

		mov cx,[pxe_udp_read_pkt.buffersize]
		sub cx,2		; CX <- bytes after opcode
		jb .failure		; Garbled reply

		mov si,packet_buf
		lodsw

		cmp ax, TFTP_ERROR
		je .bailnow		; ERROR reply: don't try again

		cmp ax, TFTP_OACK
		jne .no_tsize

		; Now we need to parse the OACK packet to get the transfer
		; size.  SI -> first byte of options; CX -> byte count
.parse_oack:
		jcxz .no_tsize			; No options acked
.get_opt_name:
		mov di,si
		mov bx,si
.opt_name_loop:	lodsb
		and al,al
		jz .got_opt_name
		or al,20h			; Convert to lowercase
		stosb
		loop .opt_name_loop
		; We ran out, and no final null
		jmp .err_reply
.got_opt_name:	; si -> option value
		dec cx				; bytes left in pkt
		jz .err_reply			; Option w/o value

		; Parse option pointed to by bx; guaranteed to be
		; null-terminated.
		push cx
		push si
		mov si,bx			; -> option name
		mov bx,tftp_opt_table
		mov cx,tftp_opts
.opt_loop:
		push cx
		push si
		mov di,[bx]			; Option pointer
		mov cx,[bx+2]			; Option len
		repe cmpsb
		pop si
		pop cx
		je .get_value			; OK, known option
		add bx,6
		loop .opt_loop

		pop si
		pop cx
		jmp .err_reply			; Non-negotiated option returned

.get_value:	pop si				; si -> option value
		pop cx				; cx -> bytes left in pkt
		mov bx,[bx+4]			; Pointer to data target
		add bx,[bp-6]			; TFTP socket pointer
		xor eax,eax
		xor edx,edx
.value_loop:	lodsb
		and al,al
		jz .got_value
		sub al,'0'
		cmp al, 9
		ja .err_reply			; Not a decimal digit
		imul edx,10
		add edx,eax
		mov [bx],edx
		loop .value_loop
		; Ran out before final null, accept anyway
		jmp short .done_pkt

.got_value:
		dec cx
		jnz .get_opt_name		; Not end of packet

		; ZF == 1

		; Success, done!
.done_pkt:
		pop si			; Junk
		pop si			; We want the packet ptr in SI

		mov eax,[si+tftp_filesize]
		cmp eax,-1
		jz .no_tsize
		mov edx,eax
		shr edx,16		; DX:AX == EAX

		and eax,eax		; Set ZF depending on file size
		pop bp			; Junk
		pop bp			; Junk (retry counter)
		jz .error_si		; ZF = 1 need to free the socket
.ret:
		pop bp
		pop cx
		pop bx
		pop es
		ret

.no_tsize:
.err_reply:	; Option negotiation error.  Send ERROR reply.
		; ServerIP and gateway are already programmed in
		mov si,[bp-6]
		mov ax,[si+tftp_remoteport]
		mov word [pxe_udp_write_pkt.rport],ax
		mov word [pxe_udp_write_pkt.buffer],tftp_opt_err
		mov word [pxe_udp_write_pkt.buffersize],tftp_opt_err_len
		mov di,pxe_udp_write_pkt
		mov bx,PXENV_UDP_WRITE
		call pxenv

		; Write an error message and explode
		mov si,err_oldtftp
		call writestr
		jmp kaboom

.bailnow:	mov word [bp-2],1	; Immediate error - no retry

.failure:	pop bx			; Junk
		pop bx
		pop si
		pop ax
		dec ax			; Retry counter
		jnz .sendreq		; Try again

.error:		mov si,bx		; Socket pointer
.error_si:				; Socket pointer already in SI
		call free_socket	; ZF <- 1, SI <- 0
		jmp .ret

;
; allocate_socket: Allocate a local UDP port structure
;
;		If successful:
;		  ZF set
;		  BX     = socket pointer
;               If unsuccessful:
;                 ZF clear
;
allocate_socket:
		push cx
		mov bx,Files
		mov cx,MAX_OPEN
.check:		cmp word [bx], byte 0
		je .found
		add bx,open_file_t_size
		loop .check
		xor cx,cx			; ZF = 1
		pop cx
		ret
		; Allocate a socket number.  Socket numbers are made
		; guaranteed unique by including the socket slot number
		; (inverted, because we use the loop counter cx); add a
		; counter value to keep the numbers from being likely to
		; get immediately reused.
		;
		; The NextSocket variable also contains the top two bits
		; set.  This generates a value in the range 49152 to
		; 57343.
.found:
		dec cx
		push ax
		mov ax,[NextSocket]
		inc ax
		and ax,((1 << (13-MAX_OPEN_LG2))-1) | 0xC000
		mov [NextSocket],ax
		shl cx,13-MAX_OPEN_LG2
		add cx,ax			; ZF = 0
		xchg ch,cl			; Convert to network byte order
		mov [bx],cx			; Socket in use
		pop ax
		pop cx
		ret

;
; Free socket: socket in SI; return SI = 0, ZF = 1 for convenience
;
free_socket:
		push es
		pusha
		xor ax,ax
		mov es,ax
		mov di,si
		mov cx,tftp_pktbuf >> 1		; tftp_pktbuf is not cleared
		rep stosw
		popa
		pop es
		xor si,si
		ret

;
; parse_dotquad:
;	       Read a dot-quad pathname in DS:SI and output an IP
;	       address in EAX, with SI pointing to the first
;	       nonmatching character.
;
;	       Return CF=1 on error.
;
parse_dotquad:
		push cx
		mov cx,4
		xor eax,eax
.parseloop:
		mov ch,ah
		mov ah,al
		lodsb
		sub al,'0'
		jb .notnumeric
		cmp al,9
		ja .notnumeric
		aad				; AL += 10 * AH; AH = 0;
		xchg ah,ch
		jmp .parseloop
.notnumeric:
		cmp al,'.'-'0'
		pushf
		mov al,ah
		mov ah,ch
		xor ch,ch
		ror eax,8
		popf
		jne .error
		loop .parseloop
		jmp .done
.error:
		loop .realerror			; If CX := 1 then we're done
		clc
		jmp .done
.realerror:
		stc
.done:
		dec si				; CF unchanged!
		pop cx
		ret
;
; mangle_name: Mangle a filename pointed to by DS:SI into a buffer pointed
;	       to by ES:DI; ends on encountering any whitespace.
;	       DI is preserved.
;
;	       This verifies that a filename is < FILENAME_MAX characters
;	       and doesn't contain whitespace, and zero-pads the output buffer,
;	       so "repe cmpsb" can do a compare.
;
;	       The first four bytes of the manged name is the IP address of
;	       the download host.
;
mangle_name:
		push di
		push si
		mov eax,[ServerIP]
		cmp byte [si],0
		je .noip			; Null filename?!?!
		cmp word [si],'::'		; Leading ::?
		je .gotprefix

.more:
		inc si
		cmp byte [si],0
		je .noip
		cmp word [si],'::'
		jne .more

		; We have a :: prefix of some sort, it could be either
		; a DNS name or a dot-quad IP address.  Try the dot-quad
		; first...
.here:
		pop si
		push si
		call parse_dotquad
		jc .notdq
		cmp word [si],'::'
		je .gotprefix
.notdq:
		pop si
		push si
		call dns_resolv
		cmp word [si],'::'
		jne .noip
		and eax,eax
		jnz .gotprefix

.noip:
		pop si
		xor eax,eax
		jmp .prefix_done

.gotprefix:
		pop cx				; Adjust stack
		inc si				; Skip double colon
		inc si

.prefix_done:
		stosd				; Save IP address prefix
		mov cx,FILENAME_MAX-5

.mn_loop:
		lodsb
		cmp al,' '			; If control or space, end
		jna .mn_end
		stosb
		loop .mn_loop
.mn_end:
		inc cx				; At least one null byte
		xor ax,ax			; Zero-fill name
		rep stosb			; Doesn't do anything if CX=0
		pop di
		ret				; Done

;
; unmangle_name: Does the opposite of mangle_name; converts a DOS-mangled
;                filename to the conventional representation.  This is needed
;                for the BOOT_IMAGE= parameter for the kernel.
;                NOTE: A 13-byte buffer is mandatory, even if the string is
;                known to be shorter.
;
;                DS:SI -> input mangled file name
;                ES:DI -> output buffer
;
;                On return, DI points to the first byte after the output name,
;                which is set to a null byte.
;
unmangle_name:
		push eax
		lodsd
		and eax,eax
		jz .noip
		call gendotquad
		mov ax,'::'
		stosw
.noip:
		call strcpy
		dec di				; Point to final null byte
		pop eax
		ret

;
; pxenv
;
; This is the main PXENV+/!PXE entry point, using the PXENV+
; calling convention.  This is a separate local routine so
; we can hook special things from it if necessary.  In particular,
; some PXE stacks seem to not like being invoked from anything but
; the initial stack, so humour it.
;

pxenv:
%if USE_PXE_PROVIDED_STACK == 0
		mov [cs:PXEStack],sp
		mov [cs:PXEStack+2],ss
		lss sp,[cs:InitStack]
%endif
.jump:		call 0:pxe_thunk		; Default to calling the thunk
%if USE_PXE_PROVIDED_STACK == 0
		lss sp,[cs:PXEStack]
%endif
		cld				; Make sure DF <- 0
		ret

; Must be after function def due to NASM bug
PXENVEntry	equ pxenv.jump+1

;
; pxe_thunk
;
; Convert from the PXENV+ calling convention (BX, ES, DI) to the !PXE
; calling convention (using the stack.)
;
; This is called as a far routine so that we can just stick it into
; the PXENVEntry variable.
;
pxe_thunk:	push es
		push di
		push bx
.jump:		call 0:0
		add sp,byte 6
		cmp ax,byte 1
		cmc				; Set CF unless ax == 0
		retf

; Must be after function def due to NASM bug
PXEEntry	equ pxe_thunk.jump+1

;
; getfssec: Get multiple clusters from a file, given the starting cluster.
;
;	In this case, get multiple blocks from a specific TCP connection.
;
;  On entry:
;	ES:BX	-> Buffer
;	SI	-> TFTP socket pointer
;	CX	-> 512-byte block count; 0FFFFh = until end of file
;  On exit:
;	SI	-> TFTP socket pointer (or 0 on EOF)
;	CF = 1	-> Hit EOF
;
getfssec:	push si
		push fs
		mov di,bx
		mov bx,si
		mov ax,pktbuf_seg
		mov fs,ax

		movzx ecx,cx
		shl ecx,TFTP_BLOCKSIZE_LG2	; Convert to bytes
		jz .hit_eof			; Nothing to do?

.need_more:
		push ecx

		movzx eax,word [bx+tftp_bytesleft]
		cmp ecx,eax
		jna .ok_size
		mov ecx,eax
		jcxz .need_packet		; No bytes available?
.ok_size:

		mov ax,cx			; EAX<31:16> == ECX<31:16> == 0
		mov si,[bx+tftp_dataptr]
		sub [bx+tftp_bytesleft],cx
		fs rep movsb			; Copy from packet buffer
		mov [bx+tftp_dataptr],si

		pop ecx
		sub ecx,eax
		jnz .need_more


.hit_eof:
		pop fs
		pop si

		; Is there anything left of this?
		mov eax,[si+tftp_filesize]
		sub eax,[si+tftp_filepos]
		jnz .bytes_left	; CF <- 0

		cmp [si+tftp_bytesleft],ax
		jnz .bytes_left	; CF <- 0

		; The socket is closed and the buffer drained
		; Close socket structure and re-init for next user
		call free_socket
		stc
.bytes_left:
		ret

;
; No data in buffer, check to see if we can get a packet...
;
.need_packet:
		pop ecx
		mov eax,[bx+tftp_filesize]
		cmp eax,[bx+tftp_filepos]
		je .hit_eof			; Already EOF'd; socket already closed

		pushad
		push es
		mov si,bx
		call get_packet
		pop es
		popad

		jmp .need_more

;
; Get a fresh packet; expects fs -> pktbuf_seg and ds:si -> socket structure
;
get_packet:
		mov ax,ds
		mov es,ax

.packet_loop:
		; Start by ACKing the previous packet; this should cause the
		; next packet to be sent.
		mov cx,PKT_RETRY
		mov word [PktTimeout],PKT_TIMEOUT

.send_ack:	push cx				; <D> Retry count

		mov ax,[si+tftp_lastpkt]
		call ack_packet			; Send ACK

		; We used to test the error code here, but sometimes
		; PXE would return negative status even though we really
		; did send the ACK.  Now, just treat a failed send as
		; a normally lost packet, and let it time out in due
		; course of events.

.send_ok:	; Now wait for packet.
		mov dx,[BIOS_timer]		; Get current time

		mov cx,[PktTimeout]
.wait_data:	push cx				; <E> Timeout
		push dx				; <F> Old time

		mov bx,[si+tftp_pktbuf]
		mov [pxe_udp_read_pkt.buffer],bx
		mov [pxe_udp_read_pkt.buffer+2],fs
		mov [pxe_udp_read_pkt.buffersize],word PKTBUF_SIZE
		mov eax,[si+tftp_remoteip]
		mov [pxe_udp_read_pkt.sip],eax
		mov eax,[MyIP]
		mov [pxe_udp_read_pkt.dip],eax
		mov ax,[si+tftp_remoteport]
		mov [pxe_udp_read_pkt.rport],ax
		mov ax,[si+tftp_localport]
		mov [pxe_udp_read_pkt.lport],ax
		mov di,pxe_udp_read_pkt
		mov bx,PXENV_UDP_READ
		push si				; <G>
		call pxenv
		pop si				; <G>
		and ax,ax
		jz .recv_ok

		; No packet, or receive failure
		mov dx,[BIOS_timer]
		pop ax				; <F> Old time
		pop cx				; <E> Timeout
		cmp ax,dx			; Same time -> don't advance timeout
		je .wait_data			; Same clock tick
		loop .wait_data			; Decrease timeout

		pop cx				; <D> Didn't get any, send another ACK
		shl word [PktTimeout],1		; Exponential backoff
		loop .send_ack
		jmp kaboom			; Forget it...

.recv_ok:	pop dx				; <F>
		pop cx				; <E>

		cmp word [pxe_udp_read_pkt.buffersize],byte 4
		jb .wait_data			; Bad size for a DATA packet

		mov bx,[si+tftp_pktbuf]
		cmp word [fs:bx],TFTP_DATA	; Not a data packet?
		jne .wait_data			; Then wait for something else

		mov ax,[si+tftp_lastpkt]
		xchg ah,al			; Host byte order
		inc ax				; Which packet are we waiting for?
		xchg ah,al			; Network byte order
		cmp [fs:bx+2],ax
		je .right_packet

		; Wrong packet, ACK the packet and then try again
		; This is presumably because the ACK got lost,
		; so the server just resent the previous packet
		mov ax,[fs:bx+2]
		call ack_packet
		jmp .send_ok			; Reset timeout

.right_packet:	; It's the packet we want.  We're also EOF if the size < blocksize

		pop cx				; <D> Don't need the retry count anymore

		mov [si+tftp_lastpkt],ax	; Update last packet number

		movzx ecx,word [pxe_udp_read_pkt.buffersize]
		sub cx,byte 4			; Skip TFTP header

		; If this is a zero-length block, don't mess with the pointers,
		; since we may have just set up the previous block that way
		jz .last_block

		; Set pointer to data block
		lea ax,[bx+4]			; Data past TFTP header
		mov [si+tftp_dataptr],ax

		add [si+tftp_filepos],ecx
		mov [si+tftp_bytesleft],cx

		cmp cx,[si+tftp_blksize]	; Is it a full block?
		jb .last_block			; If so, it's not EOF

		; If we had the exact right number of bytes, always get
		; one more packet to get the (zero-byte) EOF packet and
		; close the socket.
		mov eax,[si+tftp_filepos]
		cmp [si+tftp_filesize],eax
		je .packet_loop

		ret


.last_block:	; Last block - ACK packet immediately
		mov ax,[fs:bx+2]
		call ack_packet

		; Make sure we know we are at end of file
		mov eax,[si+tftp_filepos]
		mov [si+tftp_filesize],eax

		ret

;
; ack_packet:
;
; Send ACK packet.  This is a common operation and so is worth canning.
;
; Entry:
;	SI	= TFTP block
;	AX	= Packet # to ack (network byte order)
; Exit:
;	ZF = 0 -> Error
;	All registers preserved
;
; This function uses the pxe_udp_write_pkt but not the packet_buf.
;
ack_packet:
		pushad
		mov [ack_packet_buf+2],ax	; Packet number to ack
		mov ax,[si]
		mov [pxe_udp_write_pkt.lport],ax
		mov ax,[si+tftp_remoteport]
		mov [pxe_udp_write_pkt.rport],ax
		mov eax,[si+tftp_remoteip]
		mov [pxe_udp_write_pkt.sip],eax
		xor eax,[MyIP]
		and eax,[Netmask]
		jz .nogw
		mov eax,[Gateway]
.nogw:
		mov [pxe_udp_write_pkt.gip],eax
		mov [pxe_udp_write_pkt.buffer],word ack_packet_buf
		mov [pxe_udp_write_pkt.buffersize], word 4
		mov di,pxe_udp_write_pkt
		mov bx,PXENV_UDP_WRITE
		call pxenv
		cmp ax,byte 0			; ZF = 1 if write OK
		popad
		ret

;
; unload_pxe:
;
; This function unloads the PXE and UNDI stacks and unclaims
; the memory.
;
unload_pxe:
		test byte [KeepPXE],01h		; Should we keep PXE around?
		jnz reset_pxe

		push ds
		push es

		mov ax,cs
		mov ds,ax
		mov es,ax

		mov si,new_api_unload
		cmp byte [APIVer+1],2		; Major API version >= 2?
		jae .new_api
		mov si,old_api_unload
.new_api:

.call_loop:	xor ax,ax
		lodsb
		and ax,ax
		jz .call_done
		xchg bx,ax
		mov di,pxe_unload_stack_pkt
		push di
		xor ax,ax
		mov cx,pxe_unload_stack_pkt_len >> 1
		rep stosw
		pop di
		call pxenv
		jc .cant_free
		mov ax,word [pxe_unload_stack_pkt.status]
		cmp ax,PXENV_STATUS_SUCCESS
		jne .cant_free
		jmp .call_loop

.call_done:
		mov bx,0FF00h

		mov dx,[RealBaseMem]
		cmp dx,[BIOS_fbm]		; Sanity check
		jna .cant_free
		inc bx

		; Check that PXE actually unhooked the INT 1Ah chain
		movzx eax,word [4*0x1a]
		movzx ecx,word [4*0x1a+2]
		shl ecx,4
		add eax,ecx
		shr eax,10
		cmp ax,dx			; Not in range
		jae .ok
		cmp ax,[BIOS_fbm]
		jae .cant_free
		; inc bx

.ok:
		mov [BIOS_fbm],dx
.pop_ret:
		pop es
		pop ds
		ret

.cant_free:
		mov si,cant_free_msg
		call writestr
		push ax
		xchg bx,ax
		call writehex4
		mov al,'-'
		call writechr
		pop ax
		call writehex4
		mov al,'-'
		call writechr
		mov eax,[4*0x1a]
		call writehex8
		call crlf
		jmp .pop_ret

		; We want to keep PXE around, but still we should reset
		; it to the standard bootup configuration
reset_pxe:
		push es
		push cs
		pop es
		mov bx,PXENV_UDP_CLOSE
		mov di,pxe_udp_close_pkt
		call pxenv
		pop es
		ret

;
; gendotquad
;
; Take an IP address (in network byte order) in EAX and
; output a dotted quad string to ES:DI.
; DI points to terminal null at end of string on exit.
;
gendotquad:
		push eax
		push cx
		mov cx,4
.genchar:
		push eax
		cmp al,10		; < 10?
		jb .lt10		; If so, skip first 2 digits

		cmp al,100		; < 100
		jb .lt100		; If so, skip first digit

		aam 100
		; Now AH = 100-digit; AL = remainder
		add ah,'0'
		mov [es:di],ah
		inc di

.lt100:
		aam 10
		; Now AH = 10-digit; AL = remainder
		add ah,'0'
		mov [es:di],ah
		inc di

.lt10:
		add al,'0'
		stosb
		mov al,'.'
		stosb
		pop eax
		ror eax,8	; Move next char into LSB
		loop .genchar
		dec di
		mov [es:di], byte 0
		pop cx
		pop eax
		ret
;
; uchexbytes/lchexbytes
;
; Take a number of bytes in memory and convert to upper/lower-case
; hexadecimal
;
; Input:
;	DS:SI	= input bytes
;	ES:DI	= output buffer
;	CX	= number of bytes
; Output:
;	DS:SI	= first byte after
;	ES:DI	= first byte after
;	CX = 0
;
; Trashes AX, DX
;

lchexbytes:
	mov dl,'a'-'9'-1
	jmp xchexbytes
uchexbytes:
	mov dl,'A'-'9'-1
xchexbytes:
.loop:
	lodsb
	mov ah,al
	shr al,4
	call .outchar
	mov al,ah
	call .outchar
	loop .loop
	ret
.outchar:
	and al,0Fh
	add al,'0'
	cmp al,'9'
	jna .done
	add al,dl
.done:
	stosb
	ret

;
; pxe_get_cached_info
;
; Get a DHCP packet from the PXE stack into the trackbuf.
;
; Input:
;	DL = packet type
; Output:
;	CX = buffer size
;
; Assumes CS == DS == ES.
;
pxe_get_cached_info:
		pushad
		mov di,pxe_bootp_query_pkt
		push di
		xor ax,ax
		stosw		; Status
		movzx ax,dl
		stosw		; Packet type
		mov ax,trackbufsize
		stosw		; Buffer size
		mov ax,trackbuf
		stosw		; Buffer offset
		xor ax,ax
		stosw		; Buffer segment

		pop di		; DI -> parameter set
		mov bx,PXENV_GET_CACHED_INFO
		call pxenv
		jc .err
		and ax,ax
		jnz .err

		popad
		mov cx,[pxe_bootp_query_pkt.buffersize]
		ret

.err:
		mov si,err_pxefailed
		jmp kaboom

;
; parse_dhcp
;
; Parse a DHCP packet.  This includes dealing with "overloaded"
; option fields (see RFC 2132, section 9.3)
;
; This should fill in the following global variables, if the
; information is present:
;
; MyIP		- client IP address
; ServerIP	- boot server IP address
; Netmask	- network mask
; Gateway	- default gateway router IP
; BootFile	- boot file name
; DNSServers	- DNS server IPs
; LocalDomain	- Local domain name
; MACLen, MAC	- Client identifier, if MACLen == 0
;
; This assumes the DHCP packet is in "trackbuf" and the length
; of the packet in in CX on entry.
;

parse_dhcp:
		mov byte [OverLoad],0		; Assume no overload
		mov eax, [trackbuf+bootp.yip]
		and eax, eax
		jz .noyip
		cmp al,224			; Class D or higher -> bad
		jae .noyip
		mov [MyIP], eax
.noyip:
		mov eax, [trackbuf+bootp.sip]
		and eax, eax
		jz .nosip
		cmp al,224			; Class D or higher -> bad
		jae .nosip
		mov [ServerIP], eax
.nosip:
		sub cx, bootp.options
		jbe .nooptions
		mov si, trackbuf+bootp.option_magic
		lodsd
		cmp eax, BOOTP_OPTION_MAGIC
		jne .nooptions
		call parse_dhcp_options
.nooptions:
		mov si, trackbuf+bootp.bootfile
		test byte [OverLoad],1
		jz .nofileoverload
		mov cx,128
		call parse_dhcp_options
		jmp short .parsed_file
.nofileoverload:
		cmp byte [si], 0
		jz .parsed_file			; No bootfile name
		mov di,BootFile
		mov cx,32
		rep movsd
		xor al,al
		stosb				; Null-terminate
.parsed_file:
		mov si, trackbuf+bootp.sname
		test byte [OverLoad],2
		jz .nosnameoverload
		mov cx,64
		call parse_dhcp_options
.nosnameoverload:
		ret

;
; Parse a sequence of DHCP options, pointed to by DS:SI; the field
; size is CX -- some DHCP servers leave option fields unterminated
; in violation of the spec.
;
; For parse_some_dhcp_options, DH contains the minimum value for
; the option to recognize -- this is used to restrict parsing to
; PXELINUX-specific options only.
;
parse_dhcp_options:
		xor dx,dx

parse_some_dhcp_options:
.loop:
		and cx,cx
		jz .done

		lodsb
		dec cx
		jz .done	; Last byte; must be PAD, END or malformed
		cmp al, 0	; PAD option
		je .loop
		cmp al,255	; END option
		je .done

		; Anything else will have a length field
		mov dl,al	; DL <- option number
		xor ax,ax
		lodsb		; AX <- option length
		dec cx
		sub cx,ax	; Decrement bytes left counter
		jb .done	; Malformed option: length > field size

		cmp dl,dh	; Is the option value valid?
		jb .opt_done

		mov bx,dhcp_option_list
.find_option:
		cmp bx,dhcp_option_list_end
		jae .opt_done
		cmp dl,[bx]
		je .found_option
		add bx,3
		jmp .find_option
.found_option:
		pushad
		call [bx+1]
		popad

; Fall through
		; Unknown option.  Skip to the next one.
.opt_done:
		add si,ax
		jmp .loop
.done:
		ret

		section .data
dhcp_option_list:
		section .text

%macro dopt 2
		section .data
		db %1
		dw dopt_%2
		section .text
dopt_%2:
%endmacro

;
; Parse individual DHCP options.  SI points to the option data and
; AX to the option length.  DL contains the option number.
; All registers are saved around the routine.
;
	dopt 1, subnet_mask
		mov ebx,[si]
		mov [Netmask],ebx
		ret

	dopt 3, router
		mov ebx,[si]
		mov [Gateway],ebx
		ret

	dopt 6, dns_servers
		mov cx,ax
		shr cx,2
		cmp cl,DNS_MAX_SERVERS
		jna .oklen
		mov cl,DNS_MAX_SERVERS
.oklen:
		mov di,DNSServers
		rep movsd
		mov [LastDNSServer],di
		ret

	dopt 16, local_domain
		mov bx,si
		add bx,ax
		xor ax,ax
		xchg [bx],al	; Zero-terminate option
		mov di,LocalDomain
		call dns_mangle	; Convert to DNS label set
		mov [bx],al	; Restore ending byte
		ret

	dopt 43, vendor_encaps
		mov dh,208	; Only recognize PXELINUX options
		mov cx,ax	; Length of option = max bytes to parse
		call parse_some_dhcp_options	; Parse recursive structure
		ret

	dopt 52, option_overload
		mov bl,[si]
		mov [OverLoad],bl
		ret

	dopt 54, server
		mov eax,[si]
		cmp dword [ServerIP],0
		jne .skip		; Already have a next server IP
		cmp al,224		; Class D or higher
		jae .skip
		mov [ServerIP],eax
.skip:		ret

	dopt 61, client_identifier
		cmp ax,MAC_MAX		; Too long?
		ja .skip
		cmp ax,2		; Too short?
		jb .skip
		cmp [MACLen],ah		; Only do this if MACLen == 0
		jne .skip
		push ax
		lodsb			; Client identifier type
		cmp al,[MACType]
		pop ax
		jne .skip		; Client identifier is not a MAC
		dec ax
		mov [MACLen],al
		mov di,MAC
		jmp dhcp_copyoption
.skip:		ret

	dopt 64, bootfile_name
		mov di,BootFile
		jmp dhcp_copyoption

	dopt 97, uuid_client_identifier
		cmp ax,17		; type byte + 16 bytes UUID
		jne .skip
		mov dl,[si]		; Must have type 0 == UUID
		or dl,[HaveUUID]	; Capture only the first instance
		jnz .skip
		mov byte [HaveUUID],1	; Got UUID
		mov di,UUIDType
		jmp dhcp_copyoption
.skip:		ret

	dopt 208, pxelinux_magic
		cmp al,4		; Must have length == 4
		jne .done
		cmp dword [si], htonl(0xF100747E)	; Magic number
		jne .done
		or byte [DHCPMagic],1	; Found magic #
.done:		ret

	dopt 209, pxelinux_configfile
		mov di,ConfigName
		or byte [DHCPMagic],2	; Got config file
		jmp dhcp_copyoption

	dopt 210, pxelinux_pathprefix
		mov di,PathPrefix
		or byte [DHCPMagic],4	; Got path prefix
		jmp dhcp_copyoption

	dopt 211, pxelinux_reboottime
		cmp al,4
		jne .done
		mov ebx,[si]
		xchg bl,bh		; Convert to host byte order
		rol ebx,16
		xchg bl,bh
		mov [RebootTime],ebx
		or byte [DHCPMagic],8	; Got RebootTime
.done:		ret

		; Common code for copying an option verbatim
		; Copies the option into ES:DI and null-terminates it.
		; Returns with AX=0 and SI past the option.
dhcp_copyoption:
		xchg cx,ax	; CX <- option length
		rep movsb
		xchg cx,ax	; AX <- 0
		stosb		; Null-terminate
		ret

		section .data
dhcp_option_list_end:
		section .text

		section .data
HaveUUID	db 0
uuid_dashes	db 4,2,2,2,6,0	; Bytes per UUID dashed section
		section .text

;
; genipopt
;
; Generate an ip=<client-ip>:<boot-server-ip>:<gw-ip>:<netmask>
; option into IPOption based on a DHCP packet in trackbuf.
; Assumes CS == DS == ES.
;
genipopt:
		pushad
		mov di,IPOption
		mov eax,'ip='
		stosd
		dec di
		mov eax,[MyIP]
		call gendotquad
		mov al,':'
		stosb
		mov eax,[ServerIP]
		call gendotquad
		mov al,':'
		stosb
		mov eax,[Gateway]
		call gendotquad
		mov al,':'
		stosb
		mov eax,[Netmask]
		call gendotquad	; Zero-terminates its output
		sub di,IPOption
		mov [IPOptionLen],di
		popad
		ret

;
; Call the receive loop while idle.  This is done mostly so we can respond to
; ARP messages, but perhaps in the future this can be used to do network
; console.
;
; hpa sez: people using automatic control on the serial port get very
; unhappy if we poll for ARP too often (the PXE stack is pretty slow,
; typically.)  Therefore, only poll if at least 4 BIOS timer ticks have
; passed since the last poll, and reset this when a character is
; received (RESET_IDLE).
;
%if HAVE_IDLE

reset_idle:
		push ax
		mov ax,[cs:BIOS_timer]
		mov [cs:IdleTimer],ax
		pop ax
		ret

check_for_arp:
		push ax
		mov ax,[cs:BIOS_timer]
		sub ax,[cs:IdleTimer]
		cmp ax,4
		pop ax
		jae .need_poll
		ret
.need_poll:	pushad
		push ds
		push es
		mov ax,cs
		mov ds,ax
		mov es,ax
		mov di,packet_buf
		mov [pxe_udp_read_pkt.status],al	; 0
		mov [pxe_udp_read_pkt.buffer],di
		mov [pxe_udp_read_pkt.buffer+2],ds
		mov word [pxe_udp_read_pkt.buffersize],packet_buf_size
		mov eax,[MyIP]
		mov [pxe_udp_read_pkt.dip],eax
		mov word [pxe_udp_read_pkt.lport],htons(9)	; discard port
		mov di,pxe_udp_read_pkt
		mov bx,PXENV_UDP_READ
		call pxenv
		; Ignore result...
		pop es
		pop ds
		popad
		RESET_IDLE
		ret

%endif ; HAVE_IDLE

; -----------------------------------------------------------------------------
;  Common modules
; -----------------------------------------------------------------------------

%include "getc.inc"		; getc et al
%include "conio.inc"		; Console I/O
%include "writestr.inc"		; String output
writestr	equ cwritestr
%include "writehex.inc"		; Hexadecimal output
%include "configinit.inc"	; Initialize configuration
%include "parseconfig.inc"	; High-level config file handling
%include "parsecmd.inc"		; Low-level config file handling
%include "bcopy32.inc"		; 32-bit bcopy
%include "loadhigh.inc"		; Load a file into high memory
%include "font.inc"		; VGA font stuff
%include "graphics.inc"		; VGA graphics
%include "highmem.inc"		; High memory sizing
%include "strcpy.inc"		; strcpy()
%include "rawcon.inc"		; Console I/O w/o using the console functions
%include "dnsresolv.inc"	; DNS resolver

; -----------------------------------------------------------------------------
;  Begin data section
; -----------------------------------------------------------------------------

		section .data

copyright_str   db ' Copyright (C) 1994-', year, ' H. Peter Anvin'
		db CR, LF, 0
err_bootfailed	db CR, LF, 'Boot failed: press a key to retry, or wait for reset...', CR, LF, 0
bailmsg		equ err_bootfailed
err_nopxe	db "No !PXE or PXENV+ API found; we're dead...", CR, LF, 0
err_pxefailed	db 'PXE API call failed, error ', 0
err_udpinit	db 'Failed to initialize UDP stack', CR, LF, 0
err_noconfig	db 'Unable to locate configuration file', CR, LF, 0
err_oldtftp	db 'TFTP server does not support the tsize option', CR, LF, 0
found_pxenv	db 'Found PXENV+ structure', CR, LF, 0
using_pxenv_msg db 'Old PXE API detected, using PXENV+ structure', CR, LF, 0
apiver_str	db 'PXE API version is ',0
pxeentry_msg	db 'PXE entry point found (we hope) at ', 0
pxenventry_msg	db 'PXENV entry point found (we hope) at ', 0
trymempxe_msg	db 'Scanning memory for !PXE structure... ', 0
trymempxenv_msg	db 'Scanning memory for PXENV+ structure... ', 0
undi_data_msg	  db 'UNDI data segment at:   ',0
undi_data_len_msg db 'UNDI data segment size: ',0
undi_code_msg	  db 'UNDI code segment at:   ',0
undi_code_len_msg db 'UNDI code segment size: ',0
cant_free_msg	db 'Failed to free base memory, error ', 0
notfound_msg	db 'not found', CR, LF, 0
myipaddr_msg	db 'My IP address seems to be ',0
tftpprefix_msg	db 'TFTP prefix: ', 0
localboot_msg	db 'Booting from local disk...', CR, LF, 0
trying_msg	db 'Trying to load: ', 0
fourbs_msg	db BS, BS, BS, BS, 0
default_str	db 'default', 0
syslinux_banner	db CR, LF, 'PXELINUX ', version_str, ' ', date, ' ', 0
cfgprefix	db 'pxelinux.cfg/'		; No final null!
cfgprefix_len	equ ($-cfgprefix)

;
; Command line options we'd like to take a look at
;
; mem= and vga= are handled as normal 32-bit integer values
initrd_cmd	db 'initrd='
initrd_cmd_len	equ $-initrd_cmd

; This one we make ourselves
bootif_str	db 'BOOTIF='
bootif_str_len	equ $-bootif_str
;
; Config file keyword table
;
%include "keywords.inc"

;
; Extensions to search for (in *forward* order).
; (.bs and .bss are disabled for PXELINUX, since they are not supported)
;
		align 4, db 0
exten_table:	db '.cbt'		; COMBOOT (specific)
		db '.0', 0, 0		; PXE bootstrap program
		db '.com'		; COMBOOT (same as DOS)
		db '.c32'		; COM32
exten_table_end:
		dd 0, 0			; Need 8 null bytes here

;
; PXE unload sequences
;
new_api_unload:
		db PXENV_UDP_CLOSE
		db PXENV_UNDI_SHUTDOWN
		db PXENV_UNLOAD_STACK
		db PXENV_STOP_UNDI
		db 0
old_api_unload:
		db PXENV_UDP_CLOSE
		db PXENV_UNDI_SHUTDOWN
		db PXENV_UNLOAD_STACK
		db PXENV_UNDI_CLEANUP
		db 0

;
; PXE query packets partially filled in
;
		section .bss
pxe_bootp_query_pkt:
.status:	resw 1			; Status
.packettype:	resw 1			; Boot server packet type
.buffersize:	resw 1			; Packet size
.buffer:	resw 2			; seg:off of buffer
.bufferlimit:	resw 1			; Unused

		section .data
pxe_udp_open_pkt:
.status:	dw 0			; Status
.sip:		dd 0			; Source (our) IP

pxe_udp_close_pkt:
.status:	dw 0			; Status

pxe_udp_write_pkt:
.status:	dw 0			; Status
.sip:		dd 0			; Server IP
.gip:		dd 0			; Gateway IP
.lport:		dw 0			; Local port
.rport:		dw 0			; Remote port
.buffersize:	dw 0			; Size of packet
.buffer:	dw 0, 0			; seg:off of buffer

pxe_udp_read_pkt:
.status:	dw 0			; Status
.sip:		dd 0			; Source IP
.dip:		dd 0			; Destination (our) IP
.rport:		dw 0			; Remote port
.lport:		dw 0			; Local port
.buffersize:	dw 0			; Max packet size
.buffer:	dw 0, 0			; seg:off of buffer

;
; Misc initialized (data) variables
;
		alignb 4, db 0
BaseStack	dd StackBuf		; ESP of base stack
		dw 0			; SS of base stack
NextSocket	dw 49152		; Counter for allocating socket numbers
KeepPXE		db 0			; Should PXE be kept around?

;
; TFTP commands
;
tftp_tail	db 'octet', 0				; Octet mode
tsize_str	db 'tsize' ,0				; Request size
tsize_len	equ ($-tsize_str)
		db '0', 0
blksize_str	db 'blksize', 0				; Request large blocks
blksize_len	equ ($-blksize_str)
		asciidec TFTP_LARGEBLK
		db 0
tftp_tail_len	equ ($-tftp_tail)

		alignb 2, db 0
;
; Options negotiation parsing table (string pointer, string len, offset
; into socket structure)
;
tftp_opt_table:
		dw tsize_str, tsize_len, tftp_filesize
		dw blksize_str, blksize_len, tftp_blksize
tftp_opts	equ ($-tftp_opt_table)/6

;
; Error packet to return on options negotiation error
;
tftp_opt_err	dw TFTP_ERROR				; ERROR packet
		dw TFTP_EOPTNEG				; ERROR 8: bad options
		db 'tsize option required', 0		; Error message
tftp_opt_err_len equ ($-tftp_opt_err)

		alignb 4, db 0
ack_packet_buf:	dw TFTP_ACK, 0				; TFTP ACK packet

;
; IP information (initialized to "unknown" values)
MyIP		dd 0			; My IP address
ServerIP	dd 0			; IP address of boot server
Netmask		dd 0			; Netmask of this subnet
Gateway		dd 0			; Default router
ServerPort	dw TFTP_PORT		; TFTP server port

;
; Variables that are uninitialized in SYSLINUX but initialized here
;
		alignb 4, db 0
BufSafe		dw trackbufsize/TFTP_BLOCKSIZE	; Clusters we can load into trackbuf
BufSafeSec	dw trackbufsize/512	; = how many sectors?
BufSafeBytes	dw trackbufsize		; = how many bytes?
EndOfGetCBuf	dw getcbuf+trackbufsize	; = getcbuf+BufSafeBytes
%ifndef DEPEND
%if ( trackbufsize % TFTP_BLOCKSIZE ) != 0
%error trackbufsize must be a multiple of TFTP_BLOCKSIZE
%endif
%endif
