648 lines
18 KiB
NASM
648 lines
18 KiB
NASM
TITLE MISC - Miscellanious routines for MS-DOS
|
||
NAME MISC
|
||
;
|
||
; Miscellaneous system calls most of which are CAVEAT
|
||
;
|
||
; $SLEAZEFUNC
|
||
; $SLEAZEFUNCDL
|
||
; $GET_INDOS_FLAG
|
||
; $GET_IN_VARS
|
||
; $GET_DEFAULT_DPB
|
||
; $GET_DPB
|
||
; $DISK_RESET
|
||
; $SETDPB
|
||
; $Dup_PDB
|
||
; $CREATE_PROCESS_DATA_BLOCK
|
||
; SETMEM
|
||
;
|
||
.xlist
|
||
;
|
||
; get the appropriate segment definitions
|
||
;
|
||
INCLUDE DOSSEG.ASM
|
||
|
||
CODE SEGMENT BYTE PUBLIC 'CODE'
|
||
ASSUME SS:DOSGROUP,CS:DOSGROUP
|
||
|
||
.xcref
|
||
INCLUDE DOSSYM.ASM
|
||
INCLUDE DEVSYM.ASM
|
||
.cref
|
||
.list
|
||
|
||
|
||
ifndef Kanji
|
||
Kanji equ 0
|
||
endif
|
||
|
||
ENTRYPOINTSEG EQU 0CH
|
||
MAXDIF EQU 0FFFH
|
||
SAVEXIT EQU 10
|
||
|
||
i_need LASTBUFFER,DWORD
|
||
i_need INDOS,BYTE
|
||
i_need SYSINITVAR,BYTE
|
||
i_need CurrentPDB,WORD
|
||
i_need CreatePDB,BYTE
|
||
i_need EXIT_TYPE,BYTE
|
||
i_need EXIT_CODE,WORD
|
||
i_need LASTENT,WORD
|
||
i_need THISDPB,DWORD
|
||
i_need ATTRIB,BYTE
|
||
i_need EXTFCB,BYTE
|
||
i_need DMAADD,DWORD
|
||
i_need DIRSTART,WORD
|
||
i_need CURBUF,DWORD
|
||
i_need USER_SP,WORD
|
||
i_need ENTLAST,WORD
|
||
i_need THISDRV,BYTE
|
||
|
||
ASSUME SS:DOSGROUP
|
||
|
||
BREAK <SleazeFunc -- get a pointer to media byte>
|
||
|
||
;----+----+----+----+----+----+----+----+----+----+----+----+----+----+----;
|
||
; C A V E A T P R O G R A M M E R ;
|
||
; ;
|
||
procedure $SLEAZEFUNC,NEAR
|
||
ASSUME DS:NOTHING,ES:NOTHING
|
||
|
||
; Inputs:
|
||
; None
|
||
; Function:
|
||
; Return Stuff sort of like old get fat call
|
||
; Outputs:
|
||
; DS:BX = Points to FAT ID byte (IBM only)
|
||
; GOD help anyone who tries to do ANYTHING except
|
||
; READ this ONE byte.
|
||
; DX = Total Number of allocation units on disk
|
||
; CX = Sector size
|
||
; AL = Sectors per allocation unit
|
||
; = -1 if bad drive specified
|
||
|
||
MOV DL,0
|
||
entry $SLEAZEFUNCDL
|
||
PUSH SS
|
||
POP DS
|
||
ASSUME DS:DOSGROUP
|
||
MOV AL,DL
|
||
invoke GETTHISDRV
|
||
MOV AL,-1
|
||
JC BADSLDRIVE
|
||
invoke FATREAD
|
||
MOV DX,ES:[BP.dpb_max_cluster]
|
||
DEC DX
|
||
MOV AL,ES:[BP.dpb_cluster_mask]
|
||
INC AL
|
||
MOV CX,ES:[BP.dpb_sector_size]
|
||
ADD BP,dpb_media
|
||
BADSLDRIVE:
|
||
invoke get_user_stack
|
||
ASSUME DS:NOTHING
|
||
MOV [SI.user_CX],CX
|
||
MOV [SI.user_DX],DX
|
||
MOV [SI.user_BX],BP
|
||
MOV [SI.user_DS],ES
|
||
return
|
||
$SLEAZEFUNC ENDP
|
||
; ;
|
||
; C A V E A T P R O G R A M M E R ;
|
||
;----+----+----+----+----+----+----+----+----+----+----+----+----+----+----;
|
||
|
||
|
||
|
||
BREAK <$ABORT -- Terminate a process>
|
||
procedure $ABORT,NEAR
|
||
ASSUME DS:NOTHING,ES:NOTHING
|
||
|
||
; Inputs:
|
||
; CS:00 must point to valid program header block
|
||
; Function:
|
||
; Restore terminate and Cntrl-C addresses, flush buffers
|
||
; and transfer to the terminate address
|
||
; Returns:
|
||
; TO THE TERMINATE ADDRESS
|
||
|
||
XOR AL,AL
|
||
MOV [exit_type],exit_abort
|
||
|
||
;
|
||
; abort_inner must have AL set as the exit code!
|
||
;
|
||
entry abort_inner
|
||
MOV AH,[exit_type]
|
||
MOV [exit_code],AX
|
||
invoke Get_user_stack
|
||
MOV DS,[SI.user_CS] ; set up old interrupts
|
||
XOR AX,AX
|
||
MOV ES,AX
|
||
MOV SI,SAVEXIT
|
||
MOV DI,addr_int_terminate
|
||
MOVSW
|
||
MOVSW
|
||
MOVSW
|
||
MOVSW
|
||
MOVSW
|
||
MOVSW
|
||
transfer reset_environment
|
||
$ABORT ENDP
|
||
|
||
BREAK <$Dir_Search_First -- Start a directory search>
|
||
procedure $DIR_SEARCH_FIRST,NEAR
|
||
ASSUME DS:NOTHING,ES:NOTHING
|
||
|
||
; Inputs:
|
||
; DS:DX Points to unopenned FCB
|
||
; Function:
|
||
; Directory is searched for first matching entry and the directory
|
||
; entry is loaded at the disk transfer address
|
||
; Returns:
|
||
; AL = -1 if no entries matched, otherwise 0
|
||
|
||
invoke GETFILE
|
||
ASSUME DS:DOSGROUP
|
||
SAVPLCE:
|
||
; Search-for-next enters here to save place and report
|
||
; findings.
|
||
MOV DL,0 ; Do not XOR!!!
|
||
JC KILLSRCH
|
||
OR AH,AH ; Is it I/O device?
|
||
JS KILLIT ; If so, sign bit will end search
|
||
MOV AX,[LASTENT]
|
||
INC DL
|
||
KILLIT:
|
||
MOV ES:[DI.FILDIRENT],AX
|
||
MOV AX,WORD PTR [THISDPB]
|
||
MOV ES:[DI.fcb_DRVBP],AX
|
||
MOV AX,WORD PTR [THISDPB+2]
|
||
MOV ES:[DI.fcb_DRVBP+2],AX
|
||
MOV AX,[DIRSTART]
|
||
MOV ES:[DI.fcb_DRVBP+4],AX
|
||
; Information in directory entry must be copied into the first
|
||
; 33 bytes starting at the disk transfer address.
|
||
MOV SI,BX
|
||
LES DI,[DMAADD]
|
||
MOV AX,00FFH
|
||
CMP AL,[EXTFCB]
|
||
JNZ NORMFCB
|
||
STOSW
|
||
INC AL
|
||
STOSW
|
||
STOSW
|
||
MOV AL,[ATTRIB]
|
||
STOSB
|
||
NORMFCB:
|
||
MOV AL,[THISDRV]
|
||
INC AL
|
||
STOSB ; Set drive number
|
||
OR DL,DL
|
||
JZ DOSRELATIVE
|
||
MOV DS,WORD PTR [CURBUF+2]
|
||
ASSUME DS:NOTHING
|
||
DOSRELATIVE:
|
||
|
||
IF KANJI
|
||
MOVSW
|
||
CMP BYTE PTR ES:[DI-2],5
|
||
JNZ NOTKTRAN
|
||
MOV BYTE PTR ES:[DI-2],0E5H
|
||
NOTKTRAN:
|
||
MOV CX,15
|
||
ELSE
|
||
MOV CX,16
|
||
ENDIF
|
||
|
||
REP MOVSW ; Copy 32 bytes of directory entry
|
||
XOR AL,AL
|
||
return
|
||
|
||
ASSUME DS:NOTHING
|
||
KILLSRCH1:
|
||
PUSH DS
|
||
POP ES ; Make ES:DI point to the FCB
|
||
KILLSRCH:
|
||
MOV AX,-1
|
||
MOV WORD PTR ES:[DI.FILDIRENT],AX
|
||
return
|
||
$DIR_SEARCH_FIRST ENDP
|
||
|
||
BREAK <$Dir_Search_Next -- Find next matching directory entry>
|
||
procedure $DIR_SEARCH_NEXT,NEAR
|
||
ASSUME DS:NOTHING,ES:NOTHING
|
||
|
||
; Inputs:
|
||
; DS:DX points to unopenned FCB returned by $DIR_SEARCH_FIRST
|
||
; Function:
|
||
; Directory is searched for the next matching entry and the directory
|
||
; entry is loaded at the disk transfer address
|
||
; Returns:
|
||
; AL = -1 if no entries matched, otherwise 0
|
||
|
||
invoke MOVNAMENOSET
|
||
ASSUME ES:DOSGROUP
|
||
MOV DI,DX
|
||
JC NEAR PTR KILLSRCH1
|
||
MOV AX,[DI.FILDIRENT]
|
||
LES BP,DWORD PTR [DI.fcb_DRVBP]
|
||
OR AX,AX
|
||
JS NEAR PTR KILLSRCH1
|
||
MOV BX,[DI.fcb_DRVBP+4]
|
||
PUSH DX
|
||
PUSH DS
|
||
PUSH AX
|
||
MOV WORD PTR [THISDPB],BP
|
||
MOV WORD PTR [THISDPB+2],ES
|
||
invoke SetDirSrch
|
||
ASSUME DS:DOSGROUP
|
||
POP AX
|
||
MOV [ENTLAST],-1
|
||
invoke GetEnt
|
||
invoke NextEnt
|
||
POP ES
|
||
ASSUME ES:NOTHING
|
||
POP DI
|
||
JMP SAVPLCE
|
||
$DIR_SEARCH_NEXT ENDP
|
||
|
||
BREAK <$Get_FCB_File_Length -- Return size of file in current records>
|
||
procedure $GET_FCB_FILE_LENGTH,NEAR
|
||
ASSUME DS:NOTHING,ES:NOTHING
|
||
|
||
; Inputs:
|
||
; DS:DX points to unopenned FCB
|
||
; Function:
|
||
; Set random record field to size of file
|
||
; Returns:
|
||
; AL = -1 if no entries matched, otherwise 0
|
||
|
||
invoke GETFILE
|
||
ASSUME DS:DOSGROUP
|
||
MOV AL,-1
|
||
retc
|
||
ADD DI,fcb_RR ; Write size in RR field
|
||
MOV CX,WORD PTR ES:[DI.fcb_RECSIZ-fcb_RR]
|
||
OR CX,CX
|
||
JNZ RECOK
|
||
MOV CX,128
|
||
RECOK:
|
||
XOR DX,DX ; Intialize size to zero
|
||
INC SI
|
||
INC SI ; Point to length field
|
||
MOV DS,WORD PTR [CURBUF+2]
|
||
ASSUME DS:NOTHING
|
||
MOV AX,[SI+2] ; Get high word of size
|
||
DIV CX
|
||
PUSH AX ; Save high part of result
|
||
LODSW ; Get low word of size
|
||
DIV CX
|
||
OR DX,DX ; Check for zero remainder
|
||
POP DX
|
||
JZ DEVSIZ
|
||
INC AX ; Round up for partial record
|
||
JNZ DEVSIZ ; Propagate carry?
|
||
INC DX
|
||
DEVSIZ:
|
||
STOSW
|
||
MOV AX,DX
|
||
STOSB
|
||
MOV AL,0
|
||
CMP CX,64
|
||
JAE RET14 ; Only 3-byte field if fcb_RECSIZ >= 64
|
||
MOV ES:[DI],AH
|
||
RET14: return
|
||
$GET_FCB_FILE_LENGTH ENDP
|
||
|
||
BREAK <$Get_Fcb_Position -- Set random record field to current position>
|
||
procedure $GET_FCB_POSITION,NEAR
|
||
ASSUME DS:NOTHING,ES:NOTHING
|
||
|
||
; Inputs:
|
||
; DS:DX points to openned FCB
|
||
; Function:
|
||
; Sets random record field to be same as current record fields
|
||
; Returns:
|
||
; None
|
||
|
||
invoke GETREC
|
||
MOV WORD PTR [DI+fcb_RR],AX
|
||
MOV [DI+fcb_RR+2],DL
|
||
CMP [DI.fcb_RECSIZ],64
|
||
JAE RET16
|
||
MOV [DI+fcb_RR+2+1],DH ; Set 4th byte only if record size < 64
|
||
RET16: return
|
||
$GET_FCB_POSITION ENDP
|
||
|
||
BREAK <$Disk_Reset -- Flush out all dirty buffers>
|
||
procedure $DISK_RESET,NEAR
|
||
ASSUME DS:NOTHING,ES:NOTHING
|
||
|
||
; Inputs:
|
||
; None
|
||
; Function:
|
||
; Flush and invalidate all buffers
|
||
; Returns:
|
||
; Nothing
|
||
|
||
PUSH SS
|
||
POP DS
|
||
ASSUME DS:DOSGROUP
|
||
MOV AL,-1
|
||
invoke FLUSHBUF
|
||
MOV WORD PTR [LASTBUFFER+2],-1
|
||
MOV WORD PTR [LASTBUFFER],-1
|
||
invoke SETVISIT
|
||
ASSUME DS:NOTHING
|
||
NBFFR: ; Free ALL buffers
|
||
MOV [DI.VISIT],1 ; Mark as visited
|
||
CMP BYTE PTR [DI.BUFDRV],-1
|
||
JZ SKPBF ; Save a call to PLACEBUF
|
||
MOV WORD PTR [DI.BUFDRV],00FFH
|
||
invoke SCANPLACE
|
||
SKPBF:
|
||
invoke SKIPVISIT
|
||
JNZ NBFFR
|
||
return
|
||
$DISK_RESET ENDP
|
||
|
||
procedure $RAW_CON_IO,NEAR ; System call 6
|
||
ASSUME DS:NOTHING,ES:NOTHING
|
||
|
||
; Inputs:
|
||
; DL = -1 if input
|
||
; else DL is output character
|
||
; Function:
|
||
; Input or output raw character from console, no echo
|
||
; Returns:
|
||
; AL = character
|
||
|
||
MOV AL,DL
|
||
CMP AL,-1
|
||
JNZ RAWOUT
|
||
LES DI,DWORD PTR [user_SP] ; Get pointer to register save area
|
||
XOR BX,BX
|
||
invoke GET_IO_FCB
|
||
retc
|
||
MOV AH,1
|
||
invoke IOFUNC
|
||
JNZ RESFLG
|
||
invoke SPOOLINT
|
||
OR BYTE PTR ES:[DI.user_F],40H ; Set user's zero flag
|
||
XOR AL,AL
|
||
return
|
||
|
||
RESFLG:
|
||
AND BYTE PTR ES:[DI.user_F],0FFH-40H ; Reset user's zero flag
|
||
|
||
RILP:
|
||
invoke SPOOLINT
|
||
entry $RAW_CON_INPUT ; System call 7
|
||
|
||
; Inputs:
|
||
; None
|
||
; Function:
|
||
; Input raw character from console, no echo
|
||
; Returns:
|
||
; AL = character
|
||
|
||
XOR BX,BX
|
||
invoke GET_IO_FCB
|
||
retc
|
||
MOV AH,1
|
||
invoke IOFUNC
|
||
JZ RILP
|
||
XOR AH,AH
|
||
invoke IOFUNC
|
||
return
|
||
;
|
||
; Output the character in AL to stdout
|
||
;
|
||
entry RAWOUT
|
||
|
||
PUSH BX
|
||
MOV BX,1
|
||
|
||
invoke GET_IO_FCB
|
||
JC RAWRET1
|
||
|
||
TEST [SI.fcb_DEVID],080H ; output to file?
|
||
JZ RAWNORM ; if so, do normally
|
||
PUSH DS
|
||
PUSH SI
|
||
LDS SI,DWORD PTR [SI.fcb_FIRCLUS] ; output to special?
|
||
TEST BYTE PTR [SI+SDEVATT],ISSPEC
|
||
POP SI
|
||
POP DS
|
||
JZ RAWNORM ; if not, do normally
|
||
INT int_fastcon ; quickly output the char
|
||
JMP SHORT RAWRET
|
||
RAWNORM:
|
||
|
||
CALL RAWOUT3
|
||
RAWRET: CLC
|
||
RAWRET1:
|
||
POP BX
|
||
return
|
||
|
||
;
|
||
; Output the character in AL to handle in BX
|
||
;
|
||
entry RAWOUT2
|
||
|
||
invoke GET_IO_FCB
|
||
retc
|
||
RAWOUT3:
|
||
PUSH AX
|
||
JMP SHORT RAWOSTRT
|
||
ROLP:
|
||
invoke SPOOLINT
|
||
RAWOSTRT:
|
||
MOV AH,3
|
||
CALL IOFUNC
|
||
JZ ROLP
|
||
POP AX
|
||
MOV AH,2
|
||
CALL IOFUNC
|
||
CLC ; Clear carry indicating successful
|
||
return
|
||
$RAW_CON_IO ENDP
|
||
|
||
ASSUME DS:NOTHING,ES:NOTHING
|
||
; This routine is called at DOS init
|
||
|
||
procedure OUTMES,NEAR ; String output for internal messages
|
||
LODS CS:BYTE PTR [SI]
|
||
CMP AL,"$"
|
||
retz
|
||
invoke OUT
|
||
JMP SHORT OUTMES
|
||
return
|
||
OutMes ENDP
|
||
ASSUME SS:DOSGROUP
|
||
|
||
BREAK <$Parse_File_Descriptor -- Parse an arbitrary string into an FCB>
|
||
procedure $PARSE_FILE_DESCRIPTOR,NEAR
|
||
ASSUME DS:NOTHING,ES:NOTHING
|
||
|
||
; Inputs:
|
||
; DS:SI Points to a command line
|
||
; ES:DI Points to an empty FCB
|
||
; Bit 0 of AL = 1 At most one leading separator scanned off
|
||
; = 0 Parse stops if separator encountered
|
||
; Bit 1 of AL = 1 If drive field blank in command line - leave FCB
|
||
; = 0 " " " " " " - put 0 in FCB
|
||
; Bit 2 of AL = 1 If filename field blank - leave FCB
|
||
; = 0 " " " - put blanks in FCB
|
||
; Bit 3 of AL = 1 If extension field blank - leave FCB
|
||
; = 0 " " " - put blanks in FCB
|
||
; Function:
|
||
; Parse command line into FCB
|
||
; Returns:
|
||
; AL = 1 if '*' or '?' in filename or extension, 0 otherwise
|
||
; DS:SI points to first character after filename
|
||
|
||
invoke MAKEFCB
|
||
PUSH SI
|
||
invoke get_user_stack
|
||
POP [SI.user_SI]
|
||
return
|
||
$PARSE_FILE_DESCRIPTOR ENDP
|
||
|
||
BREAK <$Create_Process_Data_Block,SetMem -- Set up process data block>
|
||
;----+----+----+----+----+----+----+----+----+----+----+----+----+----+----;
|
||
; C A V E A T P R O G R A M M E R ;
|
||
; ;
|
||
procedure $Dup_PDB,NEAR
|
||
ASSUME DS:NOTHING,ES:NOTHING
|
||
MOV BYTE PTR [CreatePDB], 0FFH ; indicate a new process
|
||
$Dup_PDB ENDP
|
||
|
||
|
||
procedure $CREATE_PROCESS_DATA_BLOCK,NEAR
|
||
ASSUME DS:NOTHING,ES:NOTHING,SS:NOTHING
|
||
|
||
; Inputs:
|
||
; DX = Segment number of new base
|
||
; Function:
|
||
; Set up program base and copy term and ^C from int area
|
||
; Returns:
|
||
; None
|
||
; Called at DOS init
|
||
|
||
MOV ES,DX
|
||
TEST BYTE PTR [CreatePDB],0FFh
|
||
JZ create_PDB_old
|
||
MOV DS,[CurrentPDB]
|
||
JMP SHORT Create_copy
|
||
|
||
Create_PDB_old:
|
||
invoke get_user_stack
|
||
MOV DS,[SI.user_CS]
|
||
|
||
Create_copy:
|
||
XOR SI,SI ; copy all 80h bytes
|
||
MOV DI,SI
|
||
MOV CX,80H
|
||
REP MOVSW
|
||
|
||
TEST BYTE PTR [CreatePDB],0FFh ; Shall we create a process?
|
||
JZ Create_PDB_cont ; nope, old style call
|
||
;
|
||
; Here we set up for a new process...
|
||
;
|
||
|
||
PUSH CS
|
||
POP DS
|
||
ASSUME DS:DOSGROUP
|
||
XOR BX,BX ; dup all jfns
|
||
MOV CX,FilPerProc
|
||
|
||
Create_dup_jfn:
|
||
PUSH ES ; save new PDB
|
||
invoke get_jfn_pointer ; ES:DI is jfn
|
||
JC create_skip ; not a valid jfn
|
||
PUSH ES ; save him
|
||
PUSH DI
|
||
invoke get_sf_from_jfn ; get sf pointer
|
||
JC create_no_inc
|
||
INC ES:[DI].sf_ref_count ; new fh
|
||
|
||
create_no_inc:
|
||
POP DI
|
||
POP ES ; get old jfn
|
||
MOV AL,ES:[DI] ; get sfn
|
||
POP ES
|
||
PUSH ES
|
||
MOV AL,ES:[BX] ; copy into new place!
|
||
|
||
create_skip:
|
||
POP ES
|
||
INC BX ; next jfn...
|
||
LOOP create_dup_jfn
|
||
|
||
PUSH [CurrentPDB] ; get current process
|
||
POP BX
|
||
PUSH BX
|
||
POP ES:[PDB_Parent_PID] ; stash in child
|
||
MOV [CurrentPDB],ES
|
||
ASSUME DS:NOTHING
|
||
MOV DS,BX
|
||
;
|
||
; end of new process create
|
||
;
|
||
Create_PDB_cont:
|
||
MOV BYTE PTR [CreatePDB],0h ; reset flag
|
||
MOV AX,DS:[2] ; set up size for fall through
|
||
|
||
entry SETMEM
|
||
ASSUME DS:NOTHING,ES:NOTHING,SS:NOTHING
|
||
|
||
; Inputs:
|
||
; AX = Size of memory in paragraphs
|
||
; DX = Segment
|
||
; Function:
|
||
; Completely prepares a program base at the
|
||
; specified segment.
|
||
; Called at DOS init
|
||
; Outputs:
|
||
; DS = DX
|
||
; ES = DX
|
||
; [0] has INT int_abort
|
||
; [2] = First unavailable segment ([ENDMEM])
|
||
; [5] to [9] form a long call to the entry point
|
||
; [10] to [13] have exit address (from int_terminate)
|
||
; [14] to [17] have ctrl-C exit address (from int_ctrl_c)
|
||
; [18] to [21] have fatal error address (from int_fatal_abort)
|
||
; DX,BP unchanged. All other registers destroyed.
|
||
|
||
XOR CX,CX
|
||
MOV DS,CX
|
||
MOV ES,DX
|
||
MOV SI,addr_int_terminate
|
||
MOV DI,SAVEXIT
|
||
MOV CX,6
|
||
REP MOVSW
|
||
MOV ES:[2],AX
|
||
SUB AX,DX
|
||
CMP AX,MAXDIF
|
||
JBE HAVDIF
|
||
MOV AX,MAXDIF
|
||
HAVDIF:
|
||
MOV BX,ENTRYPOINTSEG
|
||
SUB BX,AX
|
||
MOV CL,4
|
||
SHL AX,CL
|
||
MOV DS,DX
|
||
MOV WORD PTR DS:[PDB_CPM_Call+1],AX
|
||
MOV WORD PTR DS:[PDB_CPM_Call+3],BX
|
||
MOV DS:[PDB_Exit_Call],(int_abort SHL 8) + mi_INT
|
||
MOV BYTE PTR DS:[PDB_CPM_Call],mi_Long_CALL
|
||
MOV WORD PTR DS:[PDB_Call_System],(int_command SHL 8) + mi_INT
|
||
MOV BYTE PTR DS:[PDB_Call_System+2],mi_Long_RET
|
||
return
|
||
|
||
$CREATE_PROCESS_DATA_BLOCK ENDP
|
||
do_ext
|
||
|
||
CODE ENDS
|
||
END
|
||
|